srd icon indicating copy to clipboard operation
srd copied to clipboard

:rocket: Simple Remote Data (SRD) is a fully static land compliant implementation of the Remote Data type in TypeScript

npm SRD License License

🚀 Simple Remote Data

Simple Remote Data (SRD) is a fully static land compliant implementation of the Remote Data type in TypeScript - built with Higer Kinded Types (HKT's) inspired by fp-ts and Elm Remote Data.

The idea for using HKT's in TypeScript is based on Lightweight higher-kinded polymorphism.

Static Land Compliant

Install

With yarn

yarn add srd

or if you prefer npm

npm i srd

or if you don't like bundlers, no need to install, just import directly from a CDN:

<script type="module">
  import { SRD } from 'https://cdn.skypack.dev/srd';
</script>

SRD supports CJS, UMD and ESM bundle outputs.

Examples

React Example

import React, { useState, useEffect } from 'react'
import { SRD, notAsked, loading, failure, success } from 'srd'

const App = () => {
  const [rd, setRd] = useState(notAsked())

  useEffect(() => {
    setRd(loading())
    fetch('...')
      .then((data) => setRd(success(data)))
      .catch((err) => setRd(failure(err)))
  }, [])

  return SRD.match({
    notAsked: () => <div>Empty</div>,
    loading: () => <div>Loading...</div>,
    failure: (err) => <div>{err}</div>,
    success: (data) => <div>{data}</div>,
  }, rd)
}

Typescript React Example

SRD works even better with Typescript! Declare your RD type once and have typescript powerfully infer it everywhere! Like magic!

import React, { useState, useEffect } from 'react'
import { SRD, RD, notAsked, loading, failure, success } from 'srd'
import { Person, getPerson } from './people'

const App = () => {
  const [rd, setRd] = useState<RD<string, Person>>(notAsked())

  useEffect(() => {
    setRd(loading())
    getPerson(123)
      .then((person) => setRd(success(person)))
      .catch((err) => setRd(failure(err)))
  }, [])

  return SRD.match({
    notAsked: () => <div>Empty</div>,
    loading: () => <div>Loading...</div>,
    failure: (msg) => <div>{msg}</div>,
    success: (person) => <div>{person}</div>,
  }, rd)
}

Documentation

SRD comes with many of the Static Land functions that we all know and love. Here is a breakdown of all the supported algebras and utilities:

Setoid

For comparing 2 SRD's to see if they are the same type.

*Note: This only compares the data types and not the inner value. So Success(5) != Failure(5) but Success(5) == Success(80).

equals :: (RD e a, RD e b) -> boolean
import { SRD, success, notAsked } from 'SRD'

SRD.equals(success(5), notAsked()) // false

Functor

Allowing the SRD to be mapped over by the function provided.

map :: (a -> b, RD e a) -> RD e b
import { SRD, success, loading } from 'SRD'

const double = x => x * 2
const rd1 = success(4)
const rd2 = loading()

SRD.map(double, rd1) // success(8)
SRD.map(double, rd2) // loading()

Bifunctor

Allowing the type to be bimapped over by the functions provided. Common usecase is for when you need to map and mapFailure in one shot.

bimap :: (e -> b, a -> c, RD e a) -> RD b c
import { SRD, success, failure } from 'SRD'

const double = x => x * 2
const formatErr = err => `Something went wrong: ${err}`
const rd1 = success(4)
const rd2 = failure('404 not found')

SRD.bimap(formatErr, double, rd1) // success(8)
SRD.bimap(formatErr, double, rd2) // failure('Something went wrong: 404 not found')

Apply

Apply a function wrapped in a SRD to a value wrapped in a SRD.

ap :: (RD e (a -> b), RD e a) -> RD e b
import { SRD, success, failure } from 'SRD'

const double = x => x * 2
const formatErr = err => `Something went wrong: ${err}`
const rd1 = success(4)
const rd2 = failure('404 not found')

SRD.ap(success(double), rd1)) // success(8)
SRD.ap(success(double), rd2)) // failure('404 not found')

Applicative

Always returns a success with whatever value is passed within.

of :: a -> RD e a
import { SRD } from 'SRD'

SRD.of(4) // success(4)

Alt

Provide a default value to be returned when an SRD is not a success type.

alt :: (RD e a, RD e a) -> RD e a
import { SRD, success, loading, notAsked } from 'SRD'

SRD.alt(success(4), notAsked())  // success(4)
SRD.alt(success(50), success(4)) // success(4)
SRD.alt(loading(), notAsked())   // loading()
SRD.alt(loading(), success(4))   // success(4)

Chain

Similar to map but the callback must return another SRD.

chain :: (a -> RD e b, RD e a) -> RD e b
import { SRD, success, failure, notAsked } from 'SRD'

SRD.chain(x => success(x * 2), success(4))    // success(8)
SRD.chain(x => success(x * 2), notAsked())    // notAsked()
SRD.chain(x => failure('failed'), success(4)) // failure('failed')

Match

Provide a mapper object for each SRD type and whichever type the SRD is - that function will run.

data Matcher e a ::
  { notAsked :: () -> c
  , loading :: () -> c
  , failure :: e -> c
  , success :: a -> c
  }

match :: (Matcher e a -> c, RD e a) -> c
import { SRD, success } from 'SRD'

SRD.match({
  notAsked: () => 'Empty',
  loading: () => 'Loading...',
  failure: e => `Err: ${e}`,
  success: data => `My data is ${data}`
}, success(4)) // My data is 4

Map Failure

Similar to map but instead of running the callback on a success, it calls it on a failure.

mapFailure :: (e -> b, RD e a) -> RD b a
import { SRD, success, failure } from 'SRD'

SRD.mapFailure(x => `hello ${x}`, success(4))     // success(4)
SRD.mapFailure(x => `hello ${x}`, failure('bob')) // failure('hello bob')

Map2

Similar to map but takes 2 SRD's instead of one, and if both are a success, the provided callback will be called.

map2 :: (a b -> c, RD e a, RD e b) -> RD e c
import { SRD, success, failure } from 'SRD'

SRD.map2((x, y) => x + y, success(4), success(8))     // success(12)
SRD.map2((x, y) => x + y, failure('bob'), success(8)) // failure('bob')
SRD.map2((x, y) => x + y, success(8), failure('bob')) // failure('bob')

Map3

Similar to map2 but takes 3 SRD's instead of two, and if all three are a success, the provided callback will be called.

map3 :: (a b c -> d, RD e a, RD e b, RD e c) -> RD e d
import { SRD, success, failure, notAsked, loading } from 'SRD'

const add3 = (x, y, z) = x + y + z

SRD.map3(add3, success(4), success(8), success(10))    // success(22)
SRD.map3(add3, failure('bob'), success(8), notAsked()) // failure('bob')
SRD.map3(add3, success(8), loading(), failure('bob'))  // loading()

Unwrap

Similar to alt, but unwraps the SRD from it's type and runs the callback on it. If the SRD is a success the inner value is passed to the callback and returned, any other value the default is returned.

unwrap :: (b, a -> b, RD e a) -> b
import { SRD, success, notAsked, loading } from 'SRD'

const double = x => x * 2

SRD.unwrap(6, double, success(8)) // 16
SRD.unwrap(6, double, notAsked()) // 6
SRD.unwrap(6, double, loading())  // 6

Unpack

Similar to unwrap, but takes a default thunk instead of a default value.

unpack :: (() -> b, a -> b, RD e a) -> b
import { SRD, success, notAsked, loading } from 'SRD'

const double = x => x * 2

SRD.unpack(() => 6, double, success(8)) // 16
SRD.unpack(() => 6, double, notAsked()) // 6
SRD.unpack(() => 6, double, loading())  // 6

WithDefault

Takes a default value and an SRD. If the SRD is a success then the inner value is returned, otherwise the default value is returned.

withDefault :: (a, RD e a) -> a
import { SRD, success, notAsked, loading } from 'SRD'

SRD.withDefault(4, success(8)) // 8
SRD.withDefault(4, notAsked()) // 4
SRD.withDefault(4, loading())  // 4

IsSuccess

Takes an SRD and returns a boolean if it is a success type.

isSuccess :: (RD e a) -> bool
import { SRD, success, notAsked } from 'SRD'

SRD.isSuccess(success(8)) // true
SRD.isSuccess(notAsked()) // false

IsFailure

Takes an SRD and returns a boolean if it is a failure type.

isFailure :: (RD e a) -> bool
import { SRD, success, failure } from 'SRD'

SRD.isFailure(success(8)) // false
SRD.isFailure(failure())  // true

IsNotAsked

Takes an SRD and returns a boolean if it is a notAsked type.

isNotAsked :: (RD e a) -> bool
import { SRD, success, notAsked } from 'SRD'

SRD.isNotAsked(success(8)) // false
SRD.isNotAsked(notAsked()) // true

IsLoading

Takes an SRD and returns a boolean if it is a loading type.

isLoading :: (RD e a) -> bool
import { SRD, success, loading } from 'SRD'

SRD.isLoading(success(8)) // false
SRD.isLoading(loading())  // true