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))Option
Section titled “Option”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.
Result
Section titled “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>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.
Conversion
Section titled “Conversion”O.toResult(option, () => 'missing')pipe( option, O.toResult(() => 'missing'),)R.toOption(result) // intentionally discards the errorO.transpose(optionOfResult)Asynchronous failure belongs in Task from @stopcock/async. Keeping
Option, Result, and Task separate makes evaluation and cancellation
semantics visible.