1.1.0 • Published 4 months ago

loadable.ts v1.1.0

Weekly downloads
8
License
MIT
Repository
github
Last release
4 months ago

Loadable.ts

Type-safe loading states for TypeScript

NPM version NPM downloads Build status All Contributors

Overview

This is a small util library to design type-safe loading states.

It includes:

  • Type-safe loading interfaces
  • Type-guards for loadable states
  • Useful operators for RxJS
  • Structural directives for Angular
  • Monad helpers

Getting started 🌩

npm
npm install loadable.ts
yarn
yarn add loadable.ts

Type-safe loading interfaces

It introduces a Loadable<T, E> type that represents three possible states, Loading, Success<T> or Failed<E>.

type Loadable<T, E = unknown> = Loading | Success<T> | Failed<E>;

Plain TypeScript example:

import { LOADING, success, failed, Loadable } from 'loadable.ts';

function getFoo(): Loadable<Foo> {
    if (...) {
        return LOADING;      // returns a `Loading`
    } else if (...) {
        return success(...); // returns a `Success<Foo>`
    } else {
        return failed(...);  // returns a `Failed`
    }
}

const foo: Loadable<Foo> = getFoo();

if (foo.loading) {
    // will infer to `Loading`
    console.log('Loading...');
} else if (foo.success) {
    // will infer to `Success<Foo>` and provide value object
    console.log(`Result: ${foo.value}`);
} else {
    // will infer to `Failed` and provide error object
    console.error(`Result: ${foo.error}`);
}

Type-guards

To improve semantics and code readability, we provide the following type-guards:

  • isLoading()
  • isSuccess()
  • isFailed()
import { isLoading, isSuccess, Loadable } from 'loadable.ts';

const foo: Loadable<Foo> = getFoo();

if (isLoading(foo)) {
    // will infer to `Loading`
    console.log('Loading...');
} else if (isSuccess(foo)) {
    // will infer to `Success<Foo>` and provide value object
    console.log(`Result: ${foo.value}`);
} else {
    // will infer to `Failed` and provide error object
    console.error(`Result: ${foo.error}`);
}

Usage with RxJS

We provide a mapToLoadable() operator for RxJS, which can be useful for async streams like HTTP responses.

  • It prepends the upstream Observable with a Loading state
  • It maps each result T in Observable<T> to a Success<T> state
  • It catches and maps each error E in the Observable to a Failed<E> state

Example:

function loadFoo(): Observable<Foo> {
  // ...
}

const foo$: Observable<Loadable<Foo>> = loadFoo().pipe(mapToLoadable());

// makes use of the provided type-guards
const showSpinner$ = foo$.pipe(map(isLoading));
const showError$ = foo$.pipe(map(isFailed));
const fooValue$ = foo$.pipe(filter(isSuccess), map(it => it.value));

Furthermore, we provide the following additional RxJS operators:

  • onFailed(): shorthand for filter(isFailed) and map((it) => it.error)
  • onSuccess(): shorthand for filter(isSuccess) and map((it) => it.value)
  • mapSuccess(mapFn): allows you to map the value when it is Success

Structural directives for Angular

We also provide three useful structural directives for Angular.

They all accept a Loadable<T> or Observable<Loadable<T>> input variable.

  • *ifLoaded: it will show the template when the latest value is in Loading state
  • *ifFailed: it will show the template when the latest value is in Failed state
  • *ifSuccess: it will show the template when the latest value is in Success state

Example usage:

interface Foo {
    name: string;
}

@Component({
    /* ... */
})
class MyComponent{
    public foo$: Observable<Loadable<Foo>> = ...;
    
    /* ... */
}
<!-- loading state -->
<div class="loading" *ifLoading="foo$"></div>

<!-- failed state -->
<div class="error" *ifFailed="foo$"></div>
<div class="error" *ifFailed="let error of foo$">
    {{ error }}
</div>

<!-- success state -->
<div class="result" *ifSuccess="foo$"></div>
<div class="result" *ifSuccess="let foo of foo$">
    {{ foo.name }}
</div>

Monads

If you want to apply operations to a Loadable, without the need to unwrap it, you could use the monad() helper function.

It returns the monadic variant LoadableMonad which currently provides the following operations:

  • map(fn: (value: T) => R)
  • flatMap(fn: (value: T) => Loadable<R>)

Example usage:

interface Foo {
    loadableBar: Loadable<Bar>;
}

interface Bar {
    name: string;
}


const foo: Loadable<Foo> = ...;

const barName: Loadable<string> = monad(foo)
    .flatMap(foo => foo.loadableBar)
    .map(bar => bar.name);

// this would be the same as:
const barName = isSuccess(foo) ? isSuccess(foo.value.loadableBar) ? success(foo.value.loadableBar.value.name) : foo.value.loadableBar : foo

Contributors ✨

Thanks goes to these wonderful people (emoji key):

This project follows the all-contributors specification. Contributions of any kind welcome!

1.1.0

4 months ago

1.0.1

11 months ago

1.0.0

11 months ago

1.0.0-alpha.0

11 months ago

0.5.0

11 months ago

0.3.0

2 years ago

0.4.0

2 years ago

0.2.1

3 years ago

0.2.0

3 years ago

0.1.1

3 years ago

0.1.0

3 years ago

0.0.2

4 years ago

0.0.1

4 years ago