Type-safe loading states for TypeScript
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
npm install loadable.ts
yarn add loadable.ts
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}`);
}
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}`);
}
We provide a mapToLoadable()
operator for RxJS, which can be useful for async streams like HTTP responses.
- It prepends the upstream
Observable
with aLoading
state - It maps each result
T
inObservable<T>
to aSuccess<T>
state - It catches and maps each error
E
in theObservable
to aFailed<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 forfilter(isFailed)
andmap((it) => it.error)
onSuccess()
: shorthand forfilter(isSuccess)
andmap((it) => it.value)
mapSuccess(mapFn)
: allows you to map thevalue
when it isSuccess
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 inLoading
state*ifFailed
: it will show the template when the latest value is inFailed
state*ifSuccess
: it will show the template when the latest value is inSuccess
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>
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
Thanks goes to these wonderful people (emoji key):
Dirk Luijk 💻 📖 |
Daan Scheerens 🤔 |
This project follows the all-contributors specification. Contributions of any kind welcome!