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 { pipe } from '@stopcock/fp'
import * as O from '@stopcock/fp/option'
import * as R from '@stopcock/fp/result'

Option has 29 dual operations and Result has 32. Each accepts its complete data-first argument list or returns the documented data-last operation. See the complete dual-call catalogue for every pair, including staged calls such as containsWith.

const directContains = O.containsWith(O.some(2), 2, Object.is)
const curriedContains = O.containsWith(Object.is)(2)(O.some(2))
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(3, (value: number) => value > 0)
const positiveInPipe = pipe(
3,
O.fromPredicate((value: number) => value > 0),
)

Then transform without repeatedly branching:

const numericPort = O.map(portText, Number)
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 directLabel = O.match(portText, {
none: () => 'not configured',
some: (value) => `port ${value}`,
})
const pipedLabel = pipe(
portText,
O.match({
none: () => 'not configured',
some: (value) => `port ${value}`,
}),
)

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(headers.authorization, () => 'Missing token')
const tokenInPipe = pipe(
headers.authorization,
R.fromNullable(() => 'Missing token'),
)

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(option, () => 'missing')
pipe(
option,
O.toResult(() => 'missing'),
)
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.