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>Constructors
Section titled “Constructors”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.
Transforms
Section titled “Transforms”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.
Composition
Section titled “Composition”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).
Extraction
Section titled “Extraction”getOrElse<B>(onErr: () => B): <A, E>(r: Result<A, E>) => A | Bmatch<A, E, B, C>({ err: (e: E) => B, ok: (a: A) => C }): (r: Result<A, E>) => B | CtoOption<A, E>(r: Result<A, E>): Option<A>Guards
Section titled “Guards”isOk<A, E>(r: Result<A, E>): r is Ok<A>isErr<A, E>(r: Result<A, E>): r is Err<E>Examples
Section titled “Examples”import { pipe } from '@stopcock/fp'import * as G from '@stopcock/fp/guard'import * as R from '@stopcock/fp/result'
// parse JSON safelyconst data = pipe( R.tryCatch(() => JSON.parse(rawBody)), R.map((body: { items: string[] }) => body.items), R.getOrElse((): string[] => []),)
// narrow an unknown request bodyconst body = R.fromPredicate(G.isPlainObject, () => 'expected an object')(rawBody)
// decode optional and nullable request fields without conflating their sentinelsconst limit = R.optional(decodePositiveInteger)(request.limit)const displayName = R.nullable(decodeNonBlankString)(request.displayName)
// chain validationstype 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 pipelinepipe( R.tryCatch(() => connectToDb()), R.tapErr((e) => console.error('db connection failed:', e)), R.getOrElse(() => fallbackDb),)