Skip to content

R (Result)

type Err<E> = { readonly _tag: 0; readonly error: E }
type Ok<A> = { readonly _tag: 1; readonly value: A }
type Result<A, E> = Ok<A> | Err<E>
ok<A>(value: A): Result<A, never>
err<E>(error: E): Result<never, E>
tryCatch<A>(thunk: () => A): Result<A, unknown>
fromNullable<E>(onNullish: () => E): <A>(value: A | null | undefined) => Result<NonNullable<A>, E>
fromPredicate<A, E>(predicate: (value: A) => boolean, onFalse: (value: A) => E): (value: A) => Result<A, E>

tryCatch wraps a function that might throw. Error comes back as unknown, narrow it yourself. When predicate is a type guard, fromPredicate narrows the successful value. onFalse receives the rejected value and runs only when the predicate fails.

map<A, B>(f: (a: A) => B): <E>(r: Result<A, E>) => Result<B, E>
mapErr<E, F>(f: (e: E) => F): <A>(r: Result<A, E>) => Result<A, F>
flatMap<A, B, E2>(f: (a: A) => Result<B, E2>): <E>(r: Result<A, E>) => Result<B, E | E2>
filterOrElse<A, E2>(predicate: (value: A) => boolean, onFalse: (value: A) => E2): <E>(result: Result<A, E>) => Result<A, E | E2>
tap<A>(f: (a: A) => void): <E>(r: Result<A, E>) => Result<A, E>
tapErr<E>(f: (e: E) => void): <A>(r: Result<A, E>) => Result<A, E>

filterOrElse also narrows with type guards. Existing errors pass through unchanged; neither the predicate nor onFalse runs for them.

all<T extends readonly Result<unknown, unknown>[]>(results: T): Result<ValuesOf<T>, ErrorsOf<T>>
sequence: typeof all
traverse<A, B, E>(decode: (value: A) => Result<B, E>): (values: readonly A[]) => Result<B[], E>
optional<A, B, E>(decode: (value: A) => Result<B, E>): (value: A | undefined) => Result<B | undefined, E>
nullable<A, B, E>(decode: (value: A) => Result<B, E>): (value: A | null) => Result<B | null, E>

all and its exact alias sequence keep tuple types and return the first error from left to right. traverse stops calling the decoder after its first error. Empty inputs return ok([]).

optional skips decoding only for undefined; nullable skips only for null. The other sentinel is passed to the decoder like any other value. Both curry the decoder first: R.optional(decode)(value) and R.nullable(decode)(value).

getOrElse<B>(onErr: () => B): <A, E>(r: Result<A, E>) => A | B
match<A, E, B, C>({ err: (e: E) => B, ok: (a: A) => C }): (r: Result<A, E>) => B | C
toOption<A, E>(r: Result<A, E>): Option<A>
isOk<A, E>(r: Result<A, E>): r is Ok<A>
isErr<A, E>(r: Result<A, E>): r is Err<E>
import { pipe } from '@stopcock/fp'
import * as G from '@stopcock/fp/guard'
import * as R from '@stopcock/fp/result'
// parse JSON safely
const data = pipe(
R.tryCatch(() => JSON.parse(rawBody)),
R.map((body: { items: string[] }) => body.items),
R.getOrElse((): string[] => []),
)
// narrow an unknown request body
const body = R.fromPredicate(G.isPlainObject, () => 'expected an object')(rawBody)
// decode optional and nullable request fields without conflating their sentinels
const limit = R.optional(decodePositiveInteger)(request.limit)
const displayName = R.nullable(decodeNonBlankString)(request.displayName)
// chain validations
type SignupForm = { readonly name: string; readonly email: string }
declare const formData: SignupForm
pipe(
R.ok(formData),
R.flatMap((d) => (d.name ? R.ok(d) : R.err('name required'))),
R.flatMap((d) => (d.email ? R.ok(d) : R.err('email required'))),
R.match({
err: (error) => ({ success: false as const, error }),
ok: (data) => ({ success: true as const, data }),
}),
)
// log errors without changing the pipeline
pipe(
R.tryCatch(() => connectToDb()),
R.tapErr((e) => console.error('db connection failed:', e)),
R.getOrElse(() => fallbackDb),
)