You may want to consider tools like TypeScript, which can now cover most of the use cases for results
. (IMO less ergonomically in some ways, but, 🤷)
While short of what I might call "complete", I do still use this on projects where I just want to write reasonable JS code that runs directly in the browser. It's been pretty solid, and I'll probably continue using it! But it's unlikely to change much. So, use it or don't, but don't expect much evolution beyond bug-fixes :)
Results is a tiny library bringing Discriminated Unions (aka Sum Types or Algebraic Types) to JavaScript, with match
for better program flow control.
Results ships with full-featured Maybe
(sometimes called an Option Type) and Result
unions built-in, helping you safely deal with optional data and error handling.
The goal of Results is JavaScript with fewer bugs.
$ npm install results
import { Result, Ok, Err } from 'results';
function validateNumber(number) {
if (isFinite(number)) {
return Ok(number);
} else {
return Err(`expected a finite number but got '${number}' (a '${typeof number}')`);
}
}
function computeSum(numbers) {
if (!(numbers instanceOf Array)) {
return Err(`expected an Array but got '${numbers}' (a '${typeof numbers}')`);
}
return Result.all(numbers.map(validateNumber))
.andThen(nums => nums.reduce((a, b) => a + b));
}
// Since computeSum returns a Result (eiter an Err() or an Ok()), we can match
// for it and handle all possible cases:
Result.match(computeSum([1, 2, 3, 4, -5]), {
Ok: sum => console.log(`The sum is: ${sum}`),
Err: err => console.error(`Something went wrong: ${err}`)
});
// Result is a synchronous compliment to Promise, and plays nicely with it:
fetch('http://example.com/numbers')
.then(resp => resp.json())
.then(nums => computeSum(nums).toPromise())
.then(sum => console.log(`The sum is: ${sum}`))
.catch(err => console.error(`Something went wrong: ${err}`));
import { Maybe, Some, None } from 'results';
// Take a tree of Maybe({val: any, left: Maybe, right: Maybe}) and flatten it
// into an array of values:
function flattenDepthFirst(root) {
return Maybe.match(root, {
None: () => [],
Some: node => [node.val]
.concat(flattenDepthFirst(node.left))
.concat(flattenDepthFirst(node.right))
});
}
import { Maybe, Some, None } from 'results';
function printGreeting(name) {
// get the name, or set a default if name is None()
const nameToPrint = Maybe.match(name, {
Some: n => n,
None: () => 'friend'
});
console.log(`Hello, oh wonderful ${nameToPrint}!`);
}
// The Maybe union has helpful methods, like .unwrapOr for getting the value
// with a default:
function printGreeting(name) {
const nameToPrint = name.unwrapOr('friend');
console.log(`Hello, oh wonderful ${nameToPrint}!`)
}
// For functions whose result may not be defined, using Maybe encourages the
// caller to handle all cases
function get(obj, key) {
if (obj.hasOwnProperty(key)) {
return Some(obj[key]);
} else {
return None();
}
}
import { Union } from 'results';
const HTTPVerbs = Union({
Options: {}, // the {} values are just placeholders, only the keys are used
Head: {},
Get: {},
Post: {},
Put: {},
Delete: {}
}, {
// the optional second object parameter to Union creates prototype methods:
isIdempotent() {
return HTTPVerbs.match(this, {
Post: () => false,
_: () => true // "_" is reserved as a catch-all in match
});
}
});
let myVerb = HTTPVerbs.Get();
console.log(`Get ${myVerb.isIdempotent() ? 'is' : 'is not'} idempotent.`);
// => "Get is idempotent"
myVerb = HTTPVerbs.Post();
console.log(`Post ${myVerb.isIdempotent() ? 'is' : 'is not'} idempotent.`);
// => "Post is not idempotent"
HTTPVerbs.match(myVerb, {
Delete: () => console.warn('some data was deleted!'),
_: () => null
});
While there is nothing react-specific in Results, it does enable some nice patterns:
import React from 'react';
import { Union } from 'results';
const AsyncState = Union({
Pending: {},
Success: {},
Failed: {}
});
class Spinner extends React.Component {
static propTypes = {
reqState: React.PropTypes.instanceOf(AsyncState.OptionClass)
}
render() {
return AsyncState.match(this.props.reqState, {
Pending: loaded => (
<div className="spinner overlay spinning">
<div className="spinner-animation">
Loading {loaded}%...
</div>
</div>
),
Failed: errMsg => (
<div className="spinner overlay failed">
<div className="spinner-err-message">
<h3>Failed to load :( </h3>
<p>{errMsg}</p>
</div>
</div>
),
Success: <div style={{display: 'none'}}></div>
});
}
}
Creates a discriminated union with members specified in the options
object.
Returns a union
object.
-
options
An object defining the members of the set. One member is added for each key ofoptions
, and the values are ignored. Almost any name can be used for the members except for two reserved names:toString
, which is automatically added for nicer debugging, andOptionClass
, which is used to attach the constructor function for member instances, for typechecking purposes. Union() will throw if either of those names are used as members inoptions
.
Maybe.None()
is an example of a member added viaoptions
. -
proto
will be used to set the protoype of member instances.toString
will automatically be added to the prototype by default, but if you define it inproto
it will override the built-in implementations.Result.Ok(1).toPromise()
is an example of a method attached throughproto
. -
static_
likeproto
but for the object returned byUnion()
: functions defined here can inspect the union, like accessingthis.OptionClass
. By default,toString
is added for you, but defining it instatic_
will override the default implementation. Union() will throw if a key instatic_
already exists inoptions
.Result.all()
is an example of a function attached throughstatic_
. -
factory
is not stable and should not be considered part of the public API :) It is used internally byMaybe
andResult
, check the source if you want to get down and dirty.
Deeply checks two union option members. This passes if:
first
andsecond
are strictly equal (===
), or- They are instances of the same
UnionOptionClass
, and- They are the same member of the
UnionOptionClass
, and - Each matching payload parameter satisfies:
- A recursive check of equality as defined by
Union.is
- A recursive check of equality as defined by
- They are the same member of the
- or they both implement
.valueOf
which passes strict equality, or - they both implement
.equals
andfirst.equals(second)
These criteria and the implementation are stolen borrowed from
Immutable, and
in fact results
's equality checks are compatible with Immutable's. Nesting
Immutable collections in OptionClassInstance
s, and nesting
OptionClassInstance
in immutable collections are both supported.
This compatibility is totally decoupled from immutablejs -- results
has no
dependency on immutable whatsoever.
Created by Union()
, this is an object with a key for each member of the union,
plus anything attached via static_
, which include OptionClass
and toString
by default. It is not safe to iterate the keys of a union
object.
Each member name's key maps to a factory to create a member instance, from a
constructor called OptionClass
(whose reference is also attached to the
union
object via they key "OptionClass").
Automatically attached to every union
object, .match
is a better way to
control program flow depending on which member of Union you are dealing with.
-
option
the OptionClass instance to match against, likeSome('hi')
orErr(new Error(':('))
. Ifoption
is not an instance of the union'sOptionClass
,match
will throw. -
paths
an object, mapping member names to callback functions. The object must either exhaustively cover all members in the Union with callbacks, or map zero or more members to callbacks and provide a catch-all callback for the name'_'
. If the coverage is not exhaustive, or if unrecognized names are included as keys,.match
will throw.
.match
will synchronously call the matching callback and return its result,
passing all arguments given to the Union Option as arguments to the callback.
import { Union } from 'results';
const Stoplight = Union({ // Union(), creating a `union` object called StopLight.
Red: {},
Amber: {},
Green: {}
});
Stoplight.match(Stoplight.Green(), {
Red: () => console.error('STOP!!!'),
Amber: () => console.warn('stop if you can'),
Green: () => console.info('ok, continue')
});
After creating a union
object, the .options
property references an object
containing keys for each union option specified. It's not usually that useful
unless you want to introspect the union and see what options it has -- powerful,
but usually not necessary!
A function for creating OptionClass instances. You should not call this
constructor directly -- it's exposed just for instanceof
checks.
In the Stoplight
example above, the following is ok:
assert(Stoplight.Green() instanceof Stoplight.OptionClass)
Attached to union
objects by keys named after the union's members. These
functions create the "values" used in result. Maybe.Some()
, Maybe.None()
,
Result.Ok()
, and Result.Err()
are all OptionClass factories. In the
Stoplight
example above, Stoplight.Green
is an OptionClassFactory.
payloads
a payload of any type can be passed as the only param. It will be stored on theOptionClass
instance, and is accessible via.match
. Proto methods may also extract the value for you, like.unwrap()
onMaybe
.
The values that are usually passed around when using Results. They have three properties that you should consider an implementation detail, never access directly. Custom proto methods may access these properties if they wish. The property names are:
.options
A reference to the object used to create the union withUnion()
. You can inspect its keys to find the members of this instance's union..name
The member name of this OptionClass instance.Maybe.None().name === 'None'
..payload
The payload provided toOptionClassFactory
.Stoplight.Red(1).payload
is1
.
Deep equality testing with another instance of a union option. See Union.is
above. As with Union.is
, this method is fully compatible with ImmutableJS.
An optional type.
Also exported as Some
from Results (import { Some } from 'results';
).
payload
A single parameter of any type. If it is an instance ofMaybe.OptionClass
, it will just be returned.
Also exported as None
from Results (import { None } from 'results';
).
Accepts no parameters
defers to match
(see above), but will only pass a single payload parameter to
a callback for Some (no parameters are passed to a None callback).
Like Promise.all
: takes an array of Some()
s and None()
s, and returns a
Some([unwrapped maybes])
if they are all Some()
, or None()
if any are
None()
. Values in maybes
that are not instances of Maybe.OptionClass
are
wrapped in Some()
.
maybes
an array ofSome()
s andNone()
s or any other value.
Returns None()
if value is undefined
, otherwise wraps it as Some(value)
.
Like Maybe.undefined(value)
, but returns None()
when value
is null
instead of when it is undefined
.
Like Maybe.undefined
and Maybe.null
but returns None()
when value
is
NaN
.
What you would hopefully expect :)
import { Some, None } from 'results';
assert(Some(1).isSome() && !Some(1).isNone());
assert(!None().isSome() && None().isNone());
Get the payload of a Some()
, or throw if it's None()
.
Like unwrap()
, but throws an Error(msg) if it is None()
.
msg
The message to throw with if it isNone()
import { Some, None } from 'results';
const n = Some(1).unwrap(); // n === 1
const m = None().unwrap(); // throws an Error instance
const o = Some(1).expect('msg') // o === 1
const p = None().expect('msg') // throws Error('msg')
Like unwrap()
, but returns def
instead of throwing for None()
def
A default value to use in case it'sNone()
Like unwrapOr
, but calls fn()
to get a default value for None()
fn
A callback accepting no parameters, returning a value forNone()
import { None } from 'results';
const x = None().unwrapOr('z'); // x === 'z';
const y = None().unwrapOrElse(() => new Date()); // y === the current date.
Get a Result
from a Maybe
err
an error payload forErr()
if it'sNone()
errFn
a callback to get a payload forErr()
if it'sNone()
import { Some, None } from 'results';
assert(Some(1).okOr(2).isOk() && None().okOr(2).isErr());
assert(None().okOrElse(() => 3).unwrapErr() === 3);
Like okOr
and okOrElse
, but returning a resolved or rejected promise.
import { Some, None } from 'results';
// the following will log "Some"
Some(1).promiseOr(2).then(d => console.log('Some'), e => console.error('None!'));
None().promiseOrElse(() => 1).catch(err => console.log(err)); // logs 1
other
Some()
orNone()
, or any value of any type which will be wrapped inSome()
.
Analogous to &&
and ||
:
import { Some, None } from 'results';
Some(1).and(Some(2)); // Some(2)
Some(1).and(None()); // None()
None().and(Some(2)); // None()
Some(1).or(Some(2)).or(None()); // Some(1)
None().or(Some(1)).or(Some(2)); // Some(1);
Like and
, but call a function instead of providing a hard-coded value. If fn
returns a raw value instead of a Some
or a None
, it will be wrapped in
Some()
.
fn
If called onSome
,fn
is called with the payload as a param.
Like andThen
but for Err
s.
fn
If called onErr
,fn
is called with the Error payload as a param.
Since andThen
's callback is only executed if it's Some()
and orElse
if
it's None
, these two methods can be used like .then
and .catch
from
Promise to chain data-processing tasks.
Test a condition against the payload of a Some(payload)
. If fn
returns
something false-y, None
is returned. Otherwise, the same Some(payload)
is
returned.
fn
If called onSome
,fn
is called with the payload as a param.
import { Maybe } from 'results';
const isEven = x => x % 2 === 0;
Maybe.Some(42).filter(isEven); // Some(42)
Maybe.Some(41).filter(isEven); // None()
An error-handling type.
Also exported as Ok
from Results (import { Ok } from 'results';
).
payload
A single parameter of any type. If it is an instance ofResult.OptionClass
, it will simply be returned.
Also exported as Err
from Results (import { Err } from 'results';
).
err
A single parameter of any type, but consider making it an instance ofError
to followPromise
conventions.
defers to match
(see above), but will only pass a single payload parameter to
a callback for Ok or Err.
Like Promise.all
: takes an array of Ok()
s and Err()
s, and returns a
Ok([unwrapped oks])
if they are all Ok()
, or the first Err()
if any are
Err()
. Values in results
that are not instances of Result.OptionClass
are
wrapped in Ok()
.
results
an array ofOk()
s andErr()
s or any other value.
Return a Result.Ok()
of the result of calling fnMaybeThrows()
, or catch any
error it throws and return it wrapped in Result.Err()
instead.
What you would hopefully expect :)
import { Ok, Err } from 'results';
assert(Ok(1).isOk() && !Ok(1).isErr());
assert(!Err(2).isOk() && Err(2).isErr());
Returns the payload from an Ok(payload)
, or throws Error(err)
.
Get the payload of a Ok()
, or throw payload
if it's Err(payload)
.
import { Ok, Err } from 'results';
const n = Ok(1).unwrap(); // n === 1
const m = Err(2).unwrap(); // throws an Error instance
Like unwrap()
, but returns def
instead of throwing for Err()
def
A default value to use in case it'sErr()
Like unwrapOr
, but calls fn()
to get a default value for Err()
fn
A callback accepting the err payload as a parameter, returning a value forErr()
import { Err } from 'results';
const x = Err(1).unwrapOr('z'); // x === 'z';
const y = Err(2).unwrapOrElse(e => e * 2); // y === 4.
Get a Maybe
from a Result
import { Ok, Err } from 'results';
assert(Ok(1).ok().isSome() && Err(2).err().isSome());
assert(Err(2).ok().isNone());
Like ok()
and err()
, but returning a resolved or rejected promise.
import { Ok, Err } from 'results';
// the following will log "Ok"
Ok(1).promise().then(d => console.log('Ok'), e => console.error('Err!'));
Err(2).promise().catch(n => console.log(n)); // logs 2
Err(2).promiseErr().then(n => console.log(n)); // logs 2
other
Ok()
orErr()
, or any value of any type which will be wrapped inOk()
.
Analogous to &&
and ||
:
import { Ok, Err } from 'results';
Ok(1).and(Ok(2)); // Ok(2)
Ok(1).and(Err(8)); // Err(8)
Err(8).and(Ok(2)); // Err(8)
Ok(1).or(Ok(2)).or(Err(8)); // Ok(1)
Err(8).or(Ok(1)).or(Ok(2)); // Ok(1);
Like and
, but call a function instead of providing a hard-coded value. If fn
returns a raw value instead of a Ok
or a Err
, it will be wrapped in
Ok()
.
fn
If called onOk
,fn
is called with the payload as a param.
Like andThen
but for Err
s.
fn
If called onErr
,fn
is called with the Error payload as a param.
Since andThen
's callback is only executed if it's Ok()
and orElse
if
it's Err
, these two methods can be used like .then
and .catch
from
Promise to chain data-processing tasks.
Results is written and maintained by uniphil, with help, support, and opinions from mystor.
The APIs for Maybe
, and Result
are heavily influenced by
rust's
Option
and
Result
.
See changelog.md