Skip to content

Option and Result

Use Option<A> when a value may be absent. Use Result<A, E> when a recoverable operation can explain why it failed. Both make the unhappy path part of the type instead of encoding it as null, an exception, or a success-flag convention.

import * as O from '@stopcock/fp/option'
import * as R from '@stopcock/fp/result'
type None = { readonly _tag: 0 }
type Some<A> = { readonly _tag: 1; readonly value: A }
type Option<A> = None | Some<A>

Create options at nullable and predicate boundaries:

const portText = O.fromNullable(process.env.PORT)
const positive = O.fromPredicate((value: number) => value > 0)(3)

Then transform without repeatedly branching:

import { pipe } from '@stopcock/fp'
const port = pipe(
portText,
O.map(Number),
O.filter((value) => Number.isInteger(value) && value > 0),
O.getOrElse(() => 3000),
)

mapNullable, flatMap, filter, zip, all, struct, and traverse cover the common composition shapes. Partial extraction is explicit:

const label = O.match({
none: () => 'not configured',
some: (value) => `port ${value}`,
})(portText)

none is a frozen singleton. The representation uses numeric tags: 0 is None, 1 is Some.

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>

Translate throwing and nullable APIs at the boundary:

type ParseError = {
readonly type: 'InvalidJson'
readonly cause: unknown
}
const parsed = R.tryCatch(
() => JSON.parse(input) as unknown,
(cause): ParseError => ({ type: 'InvalidJson', cause }),
)
const token = R.fromNullable(() => 'Missing token')(
headers.authorization,
)

map transforms success. mapErr transforms failure. flatMap sequences dependent work and keeps the union of possible error types.

const response = pipe(
parsed,
R.flatMap(decodeRequest),
R.flatMap(authorizeRequest),
R.match({
err: (error) => ({ status: 400, error }),
ok: (value) => ({ status: 200, value }),
}),
)

all, struct, and traverse are fail-fast and preserve tuple/object types. Use Validation when independent checks must all run and accumulate errors.

O.toResult(() => 'missing')(option)
R.toOption(result) // intentionally discards the error
O.transpose(optionOfResult)

Asynchronous failure belongs in Task from @stopcock/async. Keeping Option, Result, and Task separate makes evaluation and cancellation semantics visible.