@throned/resource-ts

npm install --save @throned/resource-ts

Usage no npm install needed!

<script type="module">
  import thronedResourceTs from 'https://cdn.skypack.dev/@throned/resource-ts';
</script>

README

throned ▲ resource-ts

Install

npm install --save @throned/resource-ts

yarn add @throned/resource-ts

Also it plays nicely with fp-ts pipe function, so if you wish yarn add fp-ts.

Intro

Resource is an ADT (Algebraic Data Type), that is heavily inspired by RemoteData from Slaying a UI antipattern articles.

Simply put, Resource is representation of some asynchronous data in type-safe way that also allows you to drop boolean flags such as isLoading, error etc.

Resource<D, E> is a sum type of four possible states: Initial, Loading, Success<D> and Failure<E>, where D - is a type of data and E - is a type of possible error.

When your resource is in Initial or Loading state it holds no data, so just use initial and loading constants. But Success holds some data and Failure holds the error, so for them you need to use success or failure function depending on result.

import {initial, loading, success, failure} from '@throned/resource-ts'

initial // {tag: 'initial'}
loading // {tag: 'loading'}
success({result: 'Blade Runner'}) // {tag: 'success', value: {result: 'Blade Runner'}}
failure(new Error('noop')) // {tag: 'failure', error: Error('nope')}

The proccess of wrapping your data into Resource called lifting. So when you do const resource = success(await res.json()) - you lift your response in Resource.

So now, when your data is lifted, we can do different type-safe manipulations with it using functions from @throned/resource-ts. But first, let's see how to unwrap your data from Resource, or, using FP terms, fold it.

Let's use React for next example of folding your data into JSX. To do that we will use fold function, it provides a type-safe way to extract value/error from your resource and enforces you to handle all possible states.

import React from 'react'
import {fold} from '@throned/resource-ts'

type Movie = {id: string; title: string}

const renderMovies = fold(
  () => 'Nothing here',
  () => 'Loading movies...',
  (movies: Movie[]) => (
    <ul>
      {movies.map((movie) => (
        <li key={movie.id}>{movie.title}</li>
      ))}
    </ul>
  ),
  (error: Error) => `Oops! ${error.message}`,
)

const Movies = () => {
  const movies: Resource<Movie[], Error> = useMovies()

  return <div>{renderMovies(movies)}</div>
}

API

Nothing there yet

Guides

Usage with fp-ts

While you can use bare @throned/resource-ts, it is recommended to use it with fp-ts library, and especially with pipe function. Almost all function in @throned/resource-ts are curried and data usually comes last and that where pipe shines. It can infer type from previously provided value.

import {of, map, tap} from '@throned/resource-ts'
import {pipe} from 'fp-ts/lib/pipeable'

type TVSeries = {title: string, network: string}
const tvSeries = of({title: 'Peaky Blinders', network: 'BBC'})

// To use map without pipe and keep types you have to provide it explicitly
const showSeries = (series: TVSeries) => console.log({series})
const getNetwork = (series: TVSeries) => series.network
const network = map(getNetwork)(tap(showSeries)(tvSeries))

// Alternatively you can use it with pipe and types will be inferred for you
const network = pipe(
  tvSeries,
  tap(series => console.log({series}))
  map(series => series.network),
)

Working with multiple resources

Let's imagine next situation

import {of} from '@throned/resource-ts'

// You have two resources
const number = of(42)
const mulOptions = of({times: 10})

// And you have multiply function
const multiply = (x: number, {times}: {times: number}) => {
  return x * times
}

There are few ways of how you can call multiply function with values of these resources

chain

import {of, chain, map, tap} from '@throned/resource-fp'
import {pipe} from 'fp-ts/lib/pipeable'

const number = of(42)
const mulOptions = of({times: 10})

const multiply = (x: number, {times}: {times: number}) => {
  return x * times
}

pipe(
  number,
  chain((x) =>
    map((options: {times: number}) => multiply(x, options))(mulOptions),
  ),
  tap(console.log), // 420
)

ap

import {of, ap, map, tap} from '@throned/resource-fp'
import {pipe} from 'fp-ts/lib/pipeable'

const number = of(42)
const mulOptions = of({times: 10})

const multiply = (x: number) => ({times}: {times: number}) => {
  return x * times
}

/**
 * You can define useful function that uses ap and map
 * To apply a function to resources
 *
 * To see why this functions is not included in the lib check the next example
 */
const lift2 = <A, B, C, E>(
  a: Resource<A, E>,
  b: Resource<B, E>,
  f: (a: A) => (b: B) => C,
): Resource<C, E> => {
  return pipe(a, map(f), ap(b))
}

tap(console.log)(lift2(number, mulOptions, multiply)) // 420

combine

import {of, combine, map, tap} from '@throned/resource-fp'
import {pipe} from 'fp-ts/lib/pipeable'

const number = of(42)
const mulOptions = of({times: 10})

const multiply = (x: number, {times}: {times: number}) => {
  return x * times
}

pipe(
  /**
   * combine is the most convenient way to work with multiple resources
   * it merges all resources into one tuple that can be used later on
   */
  combine(number, mulOptions),
  map(([number, options]) => multiply(number, options))
  tap(console.log) // 420
)

// Also you can pass more than 2 resources into combine
combine(number, mulOptions) // Resource<[number, {times: number}], unknown>
combine(number, mulOptions, of('check me')) // Resource<[number, {times: number}, string], unknown>