@featherweight/resource-ts
v0.4.3
Published
Resource ADT
Downloads
15
Readme
@featherweight/resource-ts
Intro
Resource is an ADT, that is heavily inspired by Remote Data described here.
Basically it's just a sum type of four states: Initial
, Pending
, Failed
and Succeded
.
While Succeded
and Failed
holds some data (value
and error
respectively), Initial
and Pending
are just constants.
Install
npm install --save @featherweight/resource-ts fp-ts
Don't forget to install peer dependency: fp-ts
.
Quick start
import {resource} from '@featherweight/resource-ts'
/* Request is not started yet */
let articlesR = resource.initial
/* Request is started, but not finished */
articlesR = resource.pending
/* Request is finished with value or error */
fetchArticles()
.then(value => {
articles = succeded(value)
})
.catch(error => {
articles = failed(error)
})
Resource is typed by 2 parameters: D (Data) and E (Error) - Resource<D, E>
.
import {initial, failed, pending, succeded} from '@featherweight/resource-ts'
type Article = {title: string}
let articlesR: Resource<Article[], Error>
articlesR = initial
articlesR = pending
articlesR = failed(new Error('Oops'))
articlesR = succeded([{title: '1984'}])
Previous examples show you how to wrap data into resource. Now let's see what to do with it and how to unwrap it.
import {fold} from '@featherweight/resource-ts'
function getArticles(): Resource<Article[], Error> {
/* _ _ _ */
}
/**
* fold provides a typesafe way to extract value from resource
* and it enforces you to provide handlers for all possible states
* */
const display = fold(
() => `not started`,
() => `loading...`,
error => `error: ${error.message}`,
articles => articles.map(a => a.title).join(', '),
)(await getArticles())
Now let's glue it all together in React example
import React, {useEffect, useState} from 'react'
import {resource, Resource} from '@featherweight/resource-ts'
import {fetchArticle, Article} from './api'
type ArticleR = Resource<Article, Error>
export const Article: React.FC<{id: string}> = props => {
const [state, setState] = useState<ArticleR>(resource.initial)
useEffect(() => {
fetchData()
async function fetchData() {
setState(resource.pending)
try {
const article = await fetchArticle({id: props.id})
setState(resource.succeded(article))
} catch (error) {
setState(resource.failed(error))
}
}
}, [props.id])
return (
<div>
<h2>Article {props.id}</h2>
<p>
{resource.fold(
state,
() => null,
() => 'fetching article',
e => e.message,
a => a.title,
)}
</p>
</div>
)
}
Contents
- Basics
- API
constructors
initial: Resource<any, any>
pending: Resource<any, any>
failed: (e: E) => Resource<any, E>
succeded: (d: D) => Resource<D, any>
of: (d: D) => Resource<D, any>
is
is.initial: (r: Resource<any, any>) => r is Initial
is.pending: (r: Resource<any, any>) => r is Pending
is.failed: (r: Resource<any, any>) => r is Failed
is.succeded: (r: Resource<any, any>) => r is Succeded
map: (f: (d: D) => R) => (r: Resource<D, E>) => Resource<R, E>
mapError: (f: (e: E) => E1) => (r: Resource<D, E>) => Resource<D, E1>
alt: (r1: () => Resource<D, E>) => (r: Resource<D, E>) => Resource<D, E>
bimap: (fd: (d: D) => R, fe: (e: E) => E1) => (r: Resource<D, E>) => Resource<R, E1>
chain: (f: (d: D) => Resource<R, E>) => ( r: Resource<D, E>,): Resource<R, E>
fold: ( onInitial: () => R, onPending: () => R, onFailed: (e: E) => R, onSucceded: (d: D) => R,) => (r: Resource<D, E>) => R
cata: ( fs: { initial?: () => R, pending?: () => R, failed?: (e: E) => R, succeded?: (v: D) => R } = {}) => (r: Resource<D, E>) => R | undefined
- Usage with fp-ts
- Working with multiple resources
API
constructors
There are 4 type constructors: initial
, pending
, failed
and succeded
plus one alias for succeded
-> of
initial: Resource<any, any>
import {initial} from '@featherweight/resource-ts'
const article: Resource<Article> = initial
// {_tag: 'initial'}
pending: Resource<any, any>
import {pending} from '@featherweight/resource-ts'
const article: Resource<Article> = pending
// {_tag: 'pending'}
failed: (e: E) => Resource<any, E>
import {failed} from '@featherweight/resource-ts'
const article: Resource<Article> = failed(new Error('ouch'))
// {_tag: 'failed', error: new Error('ouch')}
succeded: (d: D) => Resource<D, any>
import {succeded} from '@featherweight/resource-ts'
const article: Resource<Article> = succeded({id: '42', title: 'Hey'})
// {_tag: 'succeded', value: {id: '42', title: 'Hey'}}
of: (d: D) => Resource<D, any>
import {of} from '@featherweight/resource-ts'
const article: Resource<Article> = of({id: '0451', title: '1984'})
// {_tag: 'succeded', value: {id: '0451', title: '1984'}}
is
is
contains type guards for each resource type
is.initial: (r: Resource<any, any>) => r is Initial
is.pending: (r: Resource<any, any>) => r is Pending
is.failed: (r: Resource<any, any>) => r is Failed
is.succeded: (r: Resource<any, any>) => r is Succeded
import {is, succeded} from '@featherweight/resource-ts'
const article: Resource<Article, Error> = of({id: '0451', title: '1984'})
is.initial(article) // false
is.pending(article) // false
is.failed(article) // false
is.succeded(article) // true
map: (f: (d: D) => R) => (r: Resource<D, E>) => Resource<R, E>
import {
map,
failed,
initial,
pending,
succeded,
} from '@featherweight/resource-ts'
const double = map((n: number) => n * 2)
double(failed('ouch')) // failed('ouch')
double(initial) // initial
double(pending) // pending
double(succeded(10)) // succeded(20)
mapError: (f: (e: E) => E1) => (r: Resource<D, E>) => Resource<D, E1>
import {
mapError,
failed,
initial,
pending,
succeded,
} from '@featherweight/resource-ts'
const toUpperCaseE = mapError((s: string) => s.toUpperCase())
toUpperCaseE(failed('ouch')) // failed('OUCH')
toUpperCaseE(initial) // initial
toUpperCaseE(pending) // pending
toUpperCaseE(succeded(10)) // succeded(10)
alt: (r1: () => Resource<D, E>) => (r: Resource<D, E>) => Resource<D, E>
import {
alt,
failed,
initial,
pending,
succeded,
} from '@featherweight/resource-ts'
const alt10 = alt(() => succeded(10))
alt10(failed('ouch')) // succeded(10)
alt10(initial) // succeded(10)
alt10(pending) // succeded(10)
alt10(succeded(42)) // succeded(42)
bimap: (fd: (d: D) => R, fe: (e: E) => E1) => (r: Resource<D, E>) => Resource<R, E1>
import {
alt,
failed,
initial,
pending,
succeded,
} from '@featherweight/resource-ts'
const f = bimap((n: number) => n * 2, (s: string) => s.toUpperCase())
f(failed('ouch')) // failed('OUCH')
f(initial) // initial
f(pending) // pending
f(succeded(10)) // succeded(20)
chain: (f: (d: D) => Resource<R, E>) => ( r: Resource<D, E>,): Resource<R, E>
import {
chain,
of,
failed,
initial,
pending,
succeded,
} from '@featherweight/resource-ts'
const doubleR = chain((n: number) => of({value: n, doubled: n * 2}))
doubleR(succeded(10)) // succeded({value: 10, doubled: 20})
doubleR(initial) // initial
doubleR(pending) // pending
doubleR(failed('ouch')) // failed('ouch')
fold: ( onInitial: () => R, onPending: () => R, onFailed: (e: E) => R, onSucceded: (d: D) => R,) => (r: Resource<D, E>) => R
import {
fold,
failed,
initial,
pending,
succeded,
} from '@featherweight/resource-ts'
const handle = fold(
() => 'nothing there yet',
() => 'in progress',
(e: Error) => `error: ${e.message}`,
(n: number) => `result: ${n}`,
)
handle(failed(new Error('nope'))) // error: nope
handle(initial) // nothing there yet
handle(pending) // in progress
handle(succeded(42)) // resule: 42
cata: ( fs: { initial?: () => R, pending?: () => R, failed?: (e: E) => R, succeded?: (v: D) => R } = {}) => (r: Resource<D, E>) => R | undefined
import {
cata,
failed,
initial,
pending,
succeded,
} from '@featherweight/resource-ts'
/* works the same way as fold, but with cata you can skip states */
const handle = cata({
failed: (e: Error) => `error: ${e.message}`,
pending: () => 'in progress',
succeded: (n: number) => `result: ${n}`,
})
handle(failed(new Error('nope'))) // error: nope
handle(initial) // undefined
handle(pending) // in progress
handle(succeded(42)) // resule: 42
tap
import {
tap,
failed,
initial,
pending,
succeded,
} from '@featherweight/resource-ts'
/* executes handlers and returns the same resource */
const log = tap({
failed: (e: Error) => console.error(e),
pending: () => console.log('loading...'),
succeded: (n: number) => console.log(`result: ${n}`),
})
log(failed(new Error('nope'))) // error: nope
log(initial) // undefined
log(pending) // in progress
log(succeded(42)) // resule: 42
Usage with fp-ts
Curried functions work well with fp-ts
pipe.
import {pipe} from 'fp-ts/lib/pipeable'
import * as r from '@featherweight/resource-ts'
const user = r.of({name: 'Tom', age: 30})
const display = pipe(
user,
// fallback to unknown user
r.alt(() => r.of(({name: 'Unknown user', age: 0})))
// prepare a display name
r.map(user => `${user.name}, age of ${user.age}`),
)
// Tom, age of 30
Working with multiple resources
You can use concat methods to connect multiple resources.
Be aware that the preference of resources are: failed > pending > initial > succeded
import * as r from '@featherweight/resource-ts'
const user = r.of({name: 'John', id: '42'})
let friends = r.pending
r.concat(user, friends)
// pending
friends = r.of([{name: 'Tom', id: '5'}])
r.concat(user, friends)
// succeded([{name: 'John', id: '42'}, [{name: 'Tom', id: '5'}]])
const movie = r.failed(new Error('movie not found'))
r.concat3(user, friends, movie)
// failed('movie not found')