@throned/tsr
TypeScript icon, indicating that this package has built-in type declarations

0.2.1 • Public • Published

TSR

TSR stands for Type-Safe Resource. And Resource is an ADT (Algebraic Data Type), that is heavily inspired by Remote Data. You can read more about it in this post.

Install

You can install this package using npm or yarn. To get the full type inference with curried functions it's recommended to use pipe function from fp-ts.

npm install --save @throned/tsr fp-ts
# or using yarn
yarn add @throned/tsr fp-ts

Usage

import React, { useState, useEffect } from 'react'
import { TSR, Resource } from '@throned/tsr'

/* Define your resource type */
type Movie = { episode_id: string; title: string }
type MoviesResource = Resource<Movie[], Error>

const MoviesApp = () => {
  /* Initialize movies resource */
  const [movies, setMovies] = useState<MoviesResource>(TSR.initial)

  useEffect(() => {
    /* Set resource in loading state */
    setMovies(TSR.loading)
    /* Fetch all Star Wars films https://swapi.dev/documentation#films */
    fetch('https://swapi.dev/api/films/')
      .then(async res => {
        if (res.ok) {
          const { results } = await res.json()
          /* Set response payload */
          setMovies(TSR.success(results))
        }
        throw new Error('Not ok!')
      })
      .catch(error => {
        /* Or set the error */
        setMovies(TSR.failure(error))
      })
  }, [])

  return (
    <div>
      <h1>Star Wars films</h1>
      {TSR.fold(movies, {
        /* Unpack resource value and handle all possible states */
        initial: () => /* nothing is there */ null,
        loading: () => <div>loading movies...</div>,
        failure: error => <div>error: {error.message}</div>,
        success: movies => (
          <ul>
            {movies.map(movie => (
              <li key={movie.episode_id}>{movie.title}</li>
            ))}
          </ul>
        ),
      })}
    </div>
  )
}

You can find examples apps in Examples directory.

Resource

Simply put, Resource is representation of some asynchronous data in type-safe way that also allows you to drop boolean flags (such as isLoading or isError) and forces you to handle all possible states.

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

Constructors

To represent resource in Initial or Loading state use initial and loading constants. To represent resource in Success or Failure state use success and failure constructors.

import { of, initial, loading, failure, success, Resource } from '@throned/tsr'

initial // {tag: 'Initial'}
loading // {tag: 'Loading'}
success({ title: 'Blade Runner' }) // {tag: 'Success', value: {title: 'Blade Runner'}}
failure(new Error('noop')) // {tag: 'Failure', error: Error('nope')}

of({ title: 'Matrix' }) // works as success

Also there are two useful helper methods that can construct a resource: fromNullable and tryCatch.

fromNullable: (a: A | null | undefined) => Success<A> | Initial

import { fromNullable } from '@throned/tsr'

/* Returns Success with the value passed */
fromNullable('Matrix') // {tag: 'Success', value: {title: 'Matrix'}}

/* Returns Initial for null and undefined */
fromNullable(null) // {tag: 'Initial'}

tryCatch: (f: () => A) => Success<A> | Failure<E>

import { tryCatch } from '@throned/tsr'

const exsplosiveRandom = (): number => {
  const rand = Math.random()
  if (rand > 0.5) {
    throw new Error('Boom!')
  }
  return rand
}

/* tryCatch accepts a thunk that returns a value but may fail */
const rand: Resource<number, Error> = tryCatch(exsplosiveRandom)
rand // Success with random number or Failure(Error('Boom!'))

Working with multiple resources

Sometimes you have to work with more than one resource, so let's imagine next situation.

import { TSR } from '@throned/tsr'

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

// And you have two resources: number and times to multiply
const number = TSR.of(42)
const mulOptions = TSR.of({ times: 10 })

There are few ways of how you can call multiply function with values of these resources. These ways are described below, but feel free to skip them and see combine if you interested in the actual library approach only.

chain

import { TSR } from '@throned/tsr'
import { pipe } from 'fp-ts/pipeable'

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

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

const result = TSR.chain(number, n => TSR.map(mulOptions, o => multiply(n, o)))
TSR.tap(result, console.log) // 420

ap

import { TSR } from '@throned/tsr'

/* We have to make a curried version of multiply */
const multiply = (x: number) => ({ times }: { times: number }) => {
  return x * times
}

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

/**
 * You can define lift2 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 TSR.ap(b, TSR.map(a, f))
}

const result = lift2(number, mulOptions, multiply)
TSR.tap(result, console.log) // 420

combine

You don't have to understand FP concepts like applicative to use multiple resources, instead you can use a simple function that merges resources in a predictive way. If all passed resources are Success then result is Success that holds a tuple with all values, otherwise result is a fallback to first resource that is not Success.

import { of, combine, map, tap } from '@throned/tsr'

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

const args = TSR.combine(number, mulOptions) // Success<[number, {times: number}]>
const result = TSR.map(args, ([number, options]) => multiply(number, options))
TSR.tap(result, console.log) // 420

In addition to than combine can merge more than two resources into one.

import { combine, of, failure } from '@throned/tsr'

combine(of(1), of(2), of({ three: true })) // Resource<[number, number, {three: boolean}], unknown>
combine(of(1), of(2), of('3'), of('4'), of(true)) // Resource<[number, number, string, string, boolean], unknown>

/* Here is the example of fallback to non-Success resource */
combine(of(1), failure('noop'), of(2)) // {tag: 'Failure', error: 'noop'}

API style

All functions are available via direct import or TSR namespace.

import { of, map, TSR } from '@throned/tsr'
import { pipe } from 'fp-ts/pipeable'

/* Regular-style functions (similar to lodash) */
TSR.map(TSR.of(10), a => a * 10) // success(100)

/* Curried functions where actual resource comes last, shines with fp-ts pipe thanks to type inference */
pipe(
  of(10),
  map(a => a * 10),
) // success(100)

API

alt

ap

bimap

chain

combine

failure

fold

fromNullable

getOrElse

initial

loading

map

mapFailure

of

recover

success

tag

tap

tapFailure

tryCatch

unpackError

unpackValue

TSR

TSR.alt

TSR.ap

TSR.bimap

TSR.chain

TSR.combine

TSR.failure

TSR.fold

TSR.fromNullable

TSR.getOrElse

TSR.initial

TSR.loading

TSR.map

TSR.mapFailure

TSR.of

TSR.recover

TSR.success

TSR.tag

TSR.tap

TSR.tapFailure

TSR.tryCatch

TSR.unpackError

TSR.unpackValue

Package Sidebar

Install

npm i @throned/tsr

Weekly Downloads

4

Version

0.2.1

License

MIT

Unpacked Size

94.1 kB

Total Files

14

Last publish

Collaborators

  • dmytro.ulianov