Tour
TIP
This page gives a high-level tour of the library, for folks who already have an idea why they might like types like Maybe
, Result
, and Task
. For an introduction to the library that assumes no prior knowledge of types like these, see the Tutorial instead!
Just the API, please
If you're unsure of why you would want to use the library, you might jump down to Why do I need this?.
These examples don't cover every corner of the API; it's just here to show you what a few of the functions are like. Full API documentation is available! You can also view the source if you prefer.
Result
with a functional style
import Result, { err, map, ok, toString } from 'true-myth/result';
function fallibleCheck(isValid: boolean): Result<string, { reason: string }> {
return isValid ? ok('all fine here') : err({ reason: 'was not valid' });
}
const describe = (s: string) => 'The outcome was: ' + s;
const wentFine = fallibleCheck(true);
const mappedFine = map(describe, wentFine);
console.log(toString(mappedFine)); // "Ok(The outcome was: all fine here)"
const notGreat = fallibleCheck(false);
const mappedBad = map(describe, notGreat);
console.log(toString(mappedBad)); // "Err("was not valid")"
Maybe
with the method style
import Maybe, { just, nothing } from 'true-myth/maybe';
function safeLength(mightBeAString: Maybe<string>): Maybe<number> {
return mightBeAString.map((s) => s.length);
}
const justAString = just('a string');
const nothingHere = nothing<string>();
console.log(safeLength(justAString).toString()); // Just(8)
console.log(safeLength(nothingHere).toString()); // Nothing
Constructing Maybe
You can use Maybe.of
to construct a Maybe
from any value. It will return a Nothing
if the passed type is null
or undefined
, or a Just
otherwise.
import Maybe, { mapOr } from 'true-myth/maybe';
function acceptsANullOhNo(value: number | null): Maybe<string> {
const maybeNumber = Maybe.of(value);
return mapOr('0', (n) => n.toString(), maybeNumber);
}
Safely getting at values
The library provides smart type narrowing tools to allow you to get at the values wrapped in the type:
import { ok } from 'true-myth/result';
const theAnswer = ok(42);
const theAnswerValue = theAnswer.isOk ? theAnswer.value : 0;
However, ternaries like this can be annoying at times, and don't necessarily fit into functional composition pipelines when the expressions become more complicated. For situations like those, you can use one of the safe unwrap methods:
import { ok, unwrapOr } from 'true-myth/result';
const theAnswer = ok(42);
const theAnswerValue = unwrapOr(0, theAnswer);
You can also use TypeScript's "type narrowing" capabilities: if you check which variant you are accessing, TypeScript will "narrow" the type to that variant and allow you to access the value
directly if it is available.
import Maybe from 'true-myth/maybe';
// Maybe<string>
const couldBeSomething = Maybe.of('Hello!');
// type error, because `value` does not exist on `Nothing`:
// couldBeSomething.value;
if (couldBeSomething.isJust) {
// valid, because `couldBeSomething` is "narrowed" to `Just` here:
console.log(couldBeSomething.value);
}
This can also be convenient in functional style pipelines:
import { filter, map, pipe, prop } from 'ramda';
import * as result from 'true-myth/result';
import { unwrapErr } from 'true-myth/test-support';
function getErrorMessages(results: Array<Result<string, Error>>) {
return results
.filter(result.isErr)
.map(result.unwrapErr) // would not type-checkout with previous line
.map((error) => error.message);
}
Task
basics
A Task
is effectively the composition of a Promise
and a Result
.[1] What is more, it implements the PromiseLike
API for a Result<T, E>
, and provides an easy way to get a Promise<Result<T, E>>
if needed. This makes it a safe and flexible way to work with asynchronous computations.
You can wrap existing, non-Promise
-based async operations using the Task
constructor, much like you could with a Promise
. For example, if you have some reason to use the old XMLHttpRequest
instead of the more modern fetch
API, you can wrap it with a Task
like this:
import Task from 'true-myth/task';
interface RequestError {
cause: string;
status: number;
statusText: string;
}
interface HttpError {
text: string;
status: number;
statusText: string;
}
let xhrTask = new Task<string, RequestError | HttpError>((resolve, reject) => {
let req = new XMLHttpRequest();
req.addEventListener('load', () => {
if (req.status >= 200 && req.status < 300) {
resolve(req.responseText);
} else {
reject({
text: req.responseText,
status: req.status,
statusText: req.statusText,
});
}
});
req.addEventListener('error', () =>
reject({
cause: 'Network Error',
status: req.status,
statusText: req.statusText,
})
);
// etc. for the other error-emitting events
req.open('GET', 'https://true-myth.js.org', true);
req.send();
});
With Task
in place, you could write a single adapter for XMLHttpRequest
in one place in your app or library, which always produces a Task
safely.
With Task
’s ability to robustly handled all the error cases, you can use this just like you would a Promise
, with async
and await
, or you can use Task
’s own robust library of combinators. For example, to preserve type safety while working with a response, you might combine Task
with the excellent zod
library to handle API responses robustly, like so:
import * as task from 'true-myth/task';
import { z } from 'zod';
const User = z.object({
id: z.string().uuid(),
name: z.optional(z.string()),
birthday: z.date(),
});
const Users = z.array(User);
let usersTask = task.tryOrElse(
fetch('https://api.example.com/users'),
(httpError) => new Error('Fetch error', { cause: httpError })
).andThen((res) => tryOrElse(
res.json(),
(parseError) => new Error('Parse error', { cause: parseError })
)).andThen((json) => {
let result = Users.safeParse(json);
return result.success
? task.resolve(result.data)
: task.reject(new Error('Schema error', { cause: result.error }));
});
The resulting type here will be Task<Array<User>>, Error>
. You can then perform further operations on it using more tools like map
or match
:
usersTask.match({
Resolved: (users) => {
for (let user of users) {
console.log(user.name ?? "someone", "is", user.age, "years old");
}
},
Rejected: (error) => {
console.error(error.message, error.cause);
},
});
Alternatively, you can await
it and operate on its underlying Result
:
let users = (await usersTask).unwrapOr([]);
Curried variants
All static functions which take two or more parameters are automatically partially applied/curried so you can supply only some of the arguments as makes sense. For example, if you were using [lodash], you might have something like this:
import * as _ from 'lodash/fp';
import { just, nothing, map } from 'true-myth/maybe';
const length = (s: string) => s.length;
const even = (n: number) => n % 2 === 0;
const timesThree = (n: number) => n * 3;
const transform = _.flow(
// transform strings to their length: Just(3), Nothing, etc.
_.map(map(length)),
// drop `Nothing` instances
_.filter(_.prop('isJust')),
// get value now that it's safe to do so (TS will not allow it earlier)
_.map(_.prop('value')),
// only keep the even numbers ('fish' => 4)
_.filter(even),
// multiply by three
_.map(timesThree),
// add them up!
_.sum
);
const result = transform([
just('yay'),
nothing(),
nothing(),
just('waffles'),
just('fish'),
just('oh'),
]);
console.log(result); // 18
This makes for a much nicer API than needing to include the parameters for every function. If we didn't have the curried functions, we'd have a much, much noisier input:
import * as _ from 'lodash';
import Maybe, { map } from 'true-myth/maybe';
const length = (s: string) => s.length;
const even = (n: number) => n % 2 === 0;
const timesThree = (n: number) => n * 3;
const result = transform([
Maybe.of('yay'),
Maybe.nothing(),
Maybe.nothing(),
Maybe.of('waffles'),
Maybe.of('fish'),
Maybe.of('oh'),
]);
const transform = _.flow(
// transform strings to their length: Just(3), Nothing, etc.
(maybeStrings) => _.map(maybeStrings, (maybeString) => map(length, maybeString)),
// drop `Nothing` instances
(maybeLengths) => _.filter(maybeLengths, (maybe) => maybe.isJust),
// get value now that it's safe to do so (TS will not allow it earlier)
(justLengths) => _.map(justLengths, (maybe) => maybe.value),
// only keep the even numbers ('fish' => 4)
(lengths) => _.filter(lengths, even),
// multiply by three
(evenLengths) => _.map(evenLengths, timesThree),
// add them up!
_.sum
);
console.log(result); // 18
This "point-free" style isn't always better, but it's available for the times when it is better. (Use it judiciously!)
Implementation-wise, a
Task<T, E>
directly uses aPromise<Result<T, E>>
under the hood. It is, however, not identical with one, because it does implement thePromise
API intentionally, and its semantics are quite different: it never rejects, for example. For more, see Understanding: Task. ↩︎