Skip to content

Option operators: map, bind, match

Option FP (batteries included) — barrel export.

Re-exports everything from the sub-modules: core constructors (ofSome/ofNone) and operators for use in point-free pipelines.

Combines a tuple/array of Options, preserving heterogeneous types. Returns the first None or a Some of all values. Like Promise.all but for Option.

Two overloads:

  • Tuple overload (readonly [IOption<unknown>, ...IOption<unknown>[]]): preserves per-position types — all([ofSome(1), ofSome('hi')]) yields IOption<readonly [number, string]>.
  • Array overload (readonly IOption<T>[]): collapses every element to a single homogeneous type T, yielding IOption<T[]> - fits runtime-sized arrays that are not literal tuples.

Empty input (cross-module note): the three combiners of the library give three different types for “combine nothing” - combine([]) yields IResultOfT<[], never>, combine/all([]) yields IResultOfT<unknown[], unknown> and option/all([]) yields IOption<unknown[]>. The runtime behaviour is the same everywhere (an empty success); only the static type differs.

export function all<T extends readonly [IOption<unknown>, ...IOption<unknown>[]]>(
options: T,
): IOption<
{ [K in keyof T]: T[K] extends IOption<infer V> ? V : never }
>;
export function all<T>(options: readonly IOption<T>[]): IOption<T[]>;

Defined in: option/all.ts

import { all, ofSome, ofNone, match } from '@sandlada/result/option';
const combined = all([ofSome(1), ofSome(2), ofSome(3)]);
const size = match((arr) => arr.length, () => 0, combined);
// size === 3
const broken = all([ofSome(1), ofNone<number>(), ofSome(3)]);
const lost = match((arr) => arr.length, () => 0, broken);
// lost === 0 because broken is None

Chains an Option-returning function (monadic bind for Option). On None, short-circuits.

Data-last and curried: bind(fn)(option). The direct form bind(fn, option) is supported as well.

export function bind<T, U>(fn: (value: T) => IOption<U>): (opt: IOption<T>) => IOption<U>;
export function bind<T, U>(fn: (value: T) => IOption<U>, opt: IOption<T>): IOption<U>;

Defined in: option/bind.ts

import { bind, match, ofSome, ofNone } from '@sandlada/result/option';
const input = ofSome(21);
const chained = bind((n) => (n > 0 ? ofSome(n * 2) : ofNone<number>()), input);
const value = match((v) => v, () => 0, chained);
// value === 42
const missing = ofNone<number>();
const short = bind((n) => ofSome(n * 2), missing);
// short is None; bind short-circuits on None

Returns true if the Option is Some and the value equals target.

Data-last and curried: contains(target)(option). The direct form contains(target, option) is supported as well.

export function contains<T>(target: T): (opt: IOption<T>) => boolean;
export function contains<T>(target: T, opt: IOption<T>): boolean;

Defined in: option/contains.ts

import { contains, ofSome, ofNone } from '@sandlada/result/option';
const input = ofSome(42);
const hit = contains(42, input);
const miss = contains(0, input);
// hit === true, miss === false
const empty = ofNone<number>();
const absent = contains(42, empty);
// absent === false

Returns None if the predicate returns false. Otherwise passes through unchanged.

Data-last and curried: filter(predicate)(option). The direct form filter(predicate, option) is supported as well.

export function filter<T>(predicate: (value: T) => boolean): (opt: IOption<T>) => IOption<T>;
export function filter<T>(predicate: (value: T) => boolean, opt: IOption<T>): IOption<T>;

Defined in: option/filter.ts

import { filter, ofSome, unwrapOr } from '@sandlada/result/option';
const input = ofSome(42);
const kept = filter((n) => n > 0, input);
const value = unwrapOr(0, kept);
// value === 42
const rejected = filter((n) => n > 100, ofSome(42));
const fallback = unwrapOr(0, rejected);
// fallback === 0 because filter returned None

Flattens a nested Option: IOption<IOption<T>> → IOption<T>.

export function flatten<T>(opt: IOption<IOption<T>>): IOption<T>;

Defined in: option/flatten.ts

import { flatten, ofSome, ofNone, unwrapOr } from '@sandlada/result/option';
import type { IOption } from '@sandlada/result';
const nested = ofSome(ofSome(42));
const flat = flatten(nested);
const value = unwrapOr(0, flat);
// value === 42
const noneOuter = ofNone<IOption<number>>();
const skipped = flatten(noneOuter);
// skipped is None

Transforms the value if Some. On None, passes through unchanged.

Data-last and curried: map(fn)(option). The direct form map(fn, option) is supported as well.

export function map<T, U>(fn: (value: T) => U): (opt: IOption<T>) => IOption<U>;
export function map<T, U>(fn: (value: T) => U, opt: IOption<T>): IOption<U>;

Defined in: option/map.ts

import { map, match, ofSome, ofNone } from '@sandlada/result/option';
const input = ofSome(5);
const mapped = map((x: number) => x * 2, input);
const label = match((v) => `value:${v}`, () => 'nothing', mapped);
// label === 'value:10'
const empty = ofNone<number>();
const skipped = map((x: number) => x * 2, empty);
// skipped is None; map passes None through unchanged

Terminal — pattern-matches on both Some and None. Supports both positional (onSome, onNone, opt?) and object ({ some, none }, opt?) handler shapes, matching the convention used by match in @sandlada/result/async-option. Prefer the object form.

export function match<T, U>(
onSome: (value: T) => U,
onNone: () => U,
): (opt: IOption<T>) => U;
export function match<T, U>(
onSome: (value: T) => U,
onNone: () => U,
opt: IOption<T>,
): U;
export function match<T, U>(
handlers: MatchOptionHandlers<T, U>,
): (opt: IOption<T>) => U;
export function match<T, U>(
handlers: MatchOptionHandlers<T, U>,
opt: IOption<T>,
): U;

Defined in: option/match.ts

import { match, ofSome, ofNone } from '@sandlada/result/option';
const input = ofSome(42);
const positional = match((v) => `value: ${v}`, () => 'nothing', input);
// positional === 'value: 42'
const objectForm = match({ some: (v) => `value: ${v}`, none: () => 'nothing' }, ofNone());
// objectForm === 'nothing'

Creates a None variant of IOption — represents absence of a value.

The returned object is a singleton — every call returns the same frozen reference. The internal object is deep-frozen at module load, and the IOptionNone interface marks every field readonly, so the singleton cannot be mutated at runtime or via TypeScript.

Generic over T so the result can be substituted into any IOption<T> slot. T defaults to unknown so a bare ofNone() slots into any IOption<X> declaration via contextual typing — letting the surrounding const x: IOption<number> = ofNone() flow without an explicit generic. The T parameter is purely a type-level slot marker; the runtime payload carries no value.

export function ofNone<T = unknown>(): IOption<T>;

Defined in: option/ofNone.ts

import { ofNone, unwrapOr } from '@sandlada/result/option';
import type { IOption } from '@sandlada/result';
const typed: IOption<number> = ofNone();
const fromContext = unwrapOr(0, typed);
// fromContext === 0; contextual typing slots ofNone() into IOption<number>
const explicit = ofNone<string>();
const fallback = unwrapOr('default', explicit);
// fallback === 'default'

Creates a Some variant of IOption containing a value.

export function ofSome<T>(value: T): IOption<T>;

Defined in: option/ofSome.ts

import { ofSome, map } from '@sandlada/result/option';
const input = ofSome(42);
const doubled = map((n) => n * 2, input);
// doubled is Some(84)

Converts an IOption<T> to IResultOfT<T, E>. On Some, returns ok(value). On None, returns err(error).

Data-last and curried: okOr(error)(option). The direct form okOr(error, option) is supported as well.

export function okOr<E>(error: E): <T>(opt: IOption<T>) => IResultOfT<T, E>;
export function okOr<E, T>(error: E, opt: IOption<T>): IResultOfT<T, E>;

Defined in: option/okOr.ts

import { okOr, ofSome, ofNone } from '@sandlada/result/option';
import { match } from '@sandlada/result/operators';
const success = okOr('missing', ofSome(42));
const label = match((n) => `ok:${n}`, (e) => `err:${e}`, success);
// label === 'ok:42'
const failure = okOr('missing', ofNone<number>());
const code = match((n) => `ok:${n}`, (e) => `err:${e}`, failure);
// code === 'err:missing'

Converts an IOption<T> to IResultOfT<T, E | Error>. On Some, returns ok(value). On None, calls errorFn() and returns err(errorFn()).

The error is computed lazily - errorFn is only called when the option is None.

Throw policy: if errorFn() throws synchronously, the thrown value is captured as the result error. Because the thrown value can be any unknown, the return type is widened to IResultOfT<T, E | Error> - the runtime error may be an Error instance even when the user-declared E is something else. A thrown value that is not an Error instance is wrapped in one (the original value stays available through cause), so the declared error type holds for every value the caller can receive. Callers who want a precise error shape should wrap their errorFn body in a try/catch and return their own E instead of relying on this catch.

The function signature accepts a synchronous () => E; passing an async function is not supported - any returned Promise will be coerced via err(Promise). Bridge the async work yourself and return its settled value from a synchronous thunk.

Data-last and curried: okOrElse(errorFn)(option). The direct form okOrElse(errorFn, option) is supported as well.

export function okOrElse<E>(errorFn: () => E): <T>(opt: IOption<T>) => IResultOfT<T, E | Error>;
export function okOrElse<E, T>(errorFn: () => E, opt: IOption<T>): IResultOfT<T, E | Error>;

Defined in: option/okOrElse.ts

import { okOrElse, ofSome, ofNone } from '@sandlada/result/option';
import { match } from '@sandlada/result/operators';
const success = okOrElse(() => 'missing', ofSome(42));
const label = match((n) => String(n), (e) => String(e), success);
// label === '42'
const lazy = okOrElse(() => 'missing', ofNone<number>());
const code = match((n) => String(n), (e) => String(e), lazy);
// code === 'missing'

Falls back to an alternative IOption if the input is None. On Some, the input is passed through unchanged.

The fallback’s value type (U) is independent of the input’s value type (T), so a recovery can return a structurally broader or different value — the resulting carrier is IOption<T | U>. Pattern-matching on the result narrows to T on the original Some and to U on the recovered branch.

Curried form. The inner <T> is deferred so the option’s value type is re-inferred at every application site — orElse(fn)(IOption<User>) and orElse(fn)(IOption<number>) both typecheck.

export function orElse<U>(fn: () => IOption<U>): <T>(opt: IOption<T>) => IOption<T | U>;
export function orElse<T, U>(fn: () => IOption<U>, opt: IOption<T>): IOption<T | U>;

Defined in: option/orElse.ts

import { orElse, ofSome, ofNone, unwrapOr } from '@sandlada/result/option';
const recovered = orElse(() => ofSome(42), ofNone<number>());
const value = unwrapOr(0, recovered);
// value === 42
const passthrough = orElse(() => ofSome(99), ofSome(5));
const kept = unwrapOr(0, passthrough);
// kept === 5; orElse leaves Some unchanged

Direct form. T is inferred from the supplied option; the result widens to T | U.


Side-effect on the Some track. Calls fn with the value and passes the original Option through unchanged.

Throw policy: If fn throws, the Option converts to None (canonical tap/tee policy - see AGENTS.md).

Data-last and curried: tap(fn)(option). The direct form tap(fn, option) is supported as well.

export function tap<T>(fn: (value: T) => void): (opt: IOption<T>) => IOption<T>;
export function tap<T>(fn: (value: T) => void, opt: IOption<T>): IOption<T>;

Defined in: option/tap.ts

import { tap, ofSome, ofNone, unwrapOr } from '@sandlada/result/option';
const input = ofSome('hello');
const logged = tap((v) => { void v; }, input);
const text = unwrapOr('', logged);
// text === 'hello'; tap passes the original Some through
const missing = ofNone<string>();
const unchanged = tap((v) => { void v; }, missing);
// unchanged is None; tap does not run the side effect on None

Transposes an IOption<IResultOfT<T, E>> into IResultOfT<IOption<T>, E>.

  • Some(Ok(v)) → Ok(Some(v))
  • Some(Err(e)) → Err(e)
  • None → Ok(None)
export function transpose<T, E>(
opt: IOption<IResultOfT<T, E>>,
): IResultOfT<IOption<T>, E>;

Defined in: option/transpose.ts

import { transpose, ofSome, ofNone } from '@sandlada/result/option';
import { ok, err } from '@sandlada/result/factories';
import { match } from '@sandlada/result/operators';
import type { IResultOfT } from '@sandlada/result';
const wrapped = transpose(ofSome(ok(42)));
const success = match((opt) => opt.isSome, (e) => false, wrapped);
// wrapped is Ok(Some(42)); success === true
const errSide = transpose(ofSome(err('boom')));
const failed = match((opt) => 'ok', (e) => String(e), errSide);
// failed === 'boom'
const bare = transpose(ofNone<IResultOfT<number, string>>());
const stillOk = match((opt) => !opt.isSome, (e) => false, bare);
// bare is Ok(None); stillOk === true

Iterable overload of traverseArray: maps a generic Iterable with an Option-returning function, short-circuiting on the first None. Useful for generators, Set/Map, and custom iterables whose length isn’t known up front.

export function traverse<A, B>(
fn: (a: A) => IOption<B>,
): (items: Iterable<A>) => IOption<B[]>;
export function traverse<A, B>(
fn: (a: A) => IOption<B>,
items: Iterable<A>,
): IOption<B[]>;

Defined in: option/traverseArray.ts

import { traverse, ofSome, unwrapOr } from '@sandlada/result/option';
function* gen(): IterableIterator<number> { yield 1; yield 2; yield 3; }
const doubled = traverse((x) => ofSome(x * 2), gen());
const values = unwrapOr([], doubled);
// values === [2, 4, 6]

Traverses an array of elements, mapping them with a function that returns an Option. Short-circuits and returns None if the mapping function ever returns None. A synchronous throw from the callback — or from the iterator’s next() — is captured as None (module policy). Otherwise, returns a Some containing an array of the mapped values.

Two overloads:

  • Iterable<A> — generic stream input (generators, sets, custom iterables).
  • readonly A[] — concrete-array input with optional (a, index) callback.

fp-ts equivalent: Array.traverse(Option.Applicative)

export function traverseArray<A, B>(
fn: (a: A, i: number) => IOption<B>,
): (items: readonly A[]) => IOption<B[]>;
export function traverseArray<A, B>(
fn: (a: A, i: number) => IOption<B>,
items: readonly A[],
): IOption<B[]>;

Defined in: option/traverseArray.ts

import { traverseArray, ofSome, ofNone, unwrapOr } from '@sandlada/result/option';
const doubled = traverseArray((x) => (x > 0 ? ofSome(x * 2) : ofNone()), [1, 2, 3]);
const values = unwrapOr([], doubled);
// values === [2, 4, 6]
const broken = traverseArray((x) => (x > 0 ? ofSome(x * 2) : ofNone()), [1, -1, 3]);
const lost = unwrapOr([], broken);
// broken is None; lost === []

Extracts the value on Some, or returns a default on None. Never throws.

The default value’s type (D) is independent of the option’s value type (T), so a sentinel of a different shape — null, undefined, a default-value object, a string — can be substituted at the call site without re-typing the option. The result is T | D, narrowing to T on Some and to D on None via standard control-flow analysis at the use site.

Curried form. The inner <T> is deferred so the option’s value type is re-inferred at every application site — unwrapOr(default)(IOption<User>) and unwrapOr(default)(IOption<string>) both typecheck, and both produce T | D for their respective T.

export function unwrapOr<D>(defaultValue: D): <T>(opt: IOption<T>) => T | D;
export function unwrapOr<T, D>(defaultValue: D, opt: IOption<T>): T | D;

Defined in: option/unwrapOr.ts

import { unwrapOr, ofSome, ofNone } from '@sandlada/result/option';
const input = ofSome(42);
const value = unwrapOr(0, input);
// value === 42
const missing = ofNone<number>();
const fallback = unwrapOr(0, missing);
// fallback === 0

Direct form. T is inferred from the supplied option.


Combines N Options (N ≥ 2) with a function. If all are Some, returns Some(fn(a, b, ...)). If any is None, returns None. If the callback throws, returns None.

The arity of fn fixes the number of Options accepted — variadic via tuple inference. The catch-all mapped-type variadic handles every arity ≥ 2 in a single pair of overloads.

Design note (type safety): This file intentionally does NOT declare per-arity overloads like zipWith<A, B, R>(fn: (a, b) => R): .... TypeScript’s function-type bivariance would let a 2-argument fn match a 3-argument overload (with the third parameter silently ignored), and a 0-argument fn match a 2-argument curried form. The single variadic below avoids both holes: T is inferred from fn’s actual parameter list, and the constraint readonly [unknown, unknown, ...unknown[]] requires ≥ 2 elements.

export function zipWith<T extends readonly [unknown, unknown, ...unknown[]], R>(
fn: (...args: T) => R,
): (...options: { [K in keyof T]: IOption<T[K]> }) => IOption<R>;
export function zipWith<T extends readonly [unknown, unknown, ...unknown[]], R>(
fn: (...args: T) => R,
...options: { [K in keyof T]: IOption<T[K]> }
): IOption<R>;

Defined in: option/zipWith.ts

import { zipWith, ofSome, ofNone, unwrapOr } from '@sandlada/result/option';
const zipped = zipWith((a: number, b: string) => `${a}-${b}`, ofSome(1), ofSome('a'));
const text = unwrapOr('', zipped);
// text === '1-a'
const short = zipWith((a: number, b: number) => a + b, ofSome(1), ofNone<number>());
// short is None when any input is None