@jeengbe/prelude
v0.1.5
Published
A small, dependency-free functional programming toolkit for TypeScript.
Maintainers
Readme
A small, dependency-free functional programming toolkit for TypeScript.
It provides an Either type for representing a value that's either a success or a failure (with an async-aware EitherP counterpart for Promise-returning pipelines), and a handful of small Maybe helpers for working with values that may be undefined.
Installation
The package is published to npm and JSR as @jeengbe/prelude. Versions follow Semantic Versioning.
npm install @jeengbe/preludepnpm add @jeengbe/prelude
yarn add @jeengbe/prelude
bun add @jeengbe/prelude
deno add jsr:@jeengbe/preludeUsage
Either
Either<L, R> is a union of Left<L> and Right<R>, conventionally used to represent a failure (Left) or a success (Right).
import { Either } from '@jeengbe/prelude';
const ok: Either<string, number> = Either.right(42);
const err: Either<string, number> = Either.left('something went wrong');Because Either<L, R> is a plain union type, isLeft()/isRight() narrow it in both directions:
declare const e: Either<string, number>;
if (e.isLeft()) {
e; // Left<string>
} else {
e; // Right<number>
}Use map/leftMap/bimap to transform the contained value without unwrapping it:
Either.right(2).map((n) => n * 2); // Right(4)
Either.left('oops').leftMap((e) => e.toUpperCase()); // Left('OOPS')Use flatMap/leftFlatMap to chain further Either-returning operations:
declare function parseAge(input: string): Either<string, number>;
Either.right('42').flatMap(parseAge);Use tap/flatTap to run a side effect on the right value without altering the Either:
Either.right(user).tap((u) => console.log(`loaded user ${u.id}`));To get the values back out, get()/getLeft() return a Maybe<T> (i.e. undefined if this is the other side), getOrElse takes a fallback function for the right value, and pair() deconstructs the Either into a [Maybe<L>, Maybe<R>] tuple:
const [error, value] = result.pair();
const value2 = result.getOrElse(() => defaultValue);Either.fromMaybe and Either.cond build an Either out of a Maybe or a boolean condition, respectively:
Either.fromMaybe(maybeUser, () => 'user not found');
Either.cond(
items.length > 0,
() => 'no items',
() => items[0],
);EitherP
EitherP<L, R> it wraps a PromiseLike<Either<L, R>>: It exposes the same API as Either, and is itself directly awaitable.
import { EitherP } from '@jeengbe/prelude';
declare function fetchUser(id: string): Promise<Either<string, User>>;
const name = await EitherP.fromPromise(fetchUser('1'))
.map((u) => u.name)
.getOrElse(() => 'anonymous');Every Either method has an Async counterpart (mapAsync, flatMapAsync, tapAsync, etc.) that accepts a function returning a Promise and returns an EitherP, letting you chain asynchronous steps without leaving the Either world:
Either.right(orderId)
.flatMapAsync(async (id) =>
(await isValid(id)) ? Either.right(id) : Either.left('invalid order'),
)
.tapAsync(async (id) => audit.log(id))
.then((result) => result.get());Maybe
Maybe<T> is a type alias for T | undefined, along with a few helpers for working with values that may be missing:
isDefinednarrows aMaybe<T>toT.mapMaybetransforms the value if present, and passesundefinedthrough otherwise.flattenMaybecollapses aMaybe<Maybe<T>>into a single level.ifTrue/ifFalserun a function conditionally, returning its result orundefined.matchPairpattern-matches a[Maybe<A>, Maybe<B>]tuple against all four presence combinations.
import { ifTrue, isDefined, mapMaybe, matchPair } from '@jeengbe/prelude';
const upper = mapMaybe(name, (n) => n.toUpperCase());
if (isDefined(upper)) {
// upper: string
}
const warning = ifTrue(retries > 3, () => 'too many retries');
matchPair([error, value], {
neither: () => 'nothing to report',
a: (e) => `error: ${e}`,
b: (v) => `value: ${v}`,
both: (e, v) => `error ${e}, but got a partial value: ${v}`,
});License
MIT Jesper Engberg
