Skip to content

Option & Result

If you’ve written a function that can fail and thought “should I throw? return null? return an object with a success flag?”, these two types sort that out.

Option is for values that might not exist. Result is for operations that can fail with an error you care about. Both force you to handle the unhappy path before you can use the value. The compiler won’t let you forget.

They use numeric _tag (0 for None/Err, 1 for Some/Ok) instead of strings. V8 can branch on numbers in a single CPU instruction.

type None = { readonly _tag: 0 }
type Some<A> = { readonly _tag: 1; readonly value: A }
type Option<A> = None | Some<A>

Wraps a value that might not be there. none is a singleton.

some<A>(value: A): Option<A>
none: None
fromNullable<A>(value: A | null | undefined): Option<NonNullable<A>>
fromPredicate<A>(pred: (a: A) => boolean): (a: A) => Option<A>
map<A, B>(f: (a: A) => B): (o: Option<A>) => Option<B>
flatMap<A, B>(f: (a: A) => Option<B>): (o: Option<A>) => Option<B>
filter<A>(pred: (a: A) => boolean): (o: Option<A>) => Option<A>
tap<A>(f: (a: A) => void): (o: Option<A>) => Option<A>
getOrElse<B>(onNone: () => B): <A>(o: Option<A>) => A | B
getWithDefault<B>(value: B): <A>(o: Option<A>) => A | B
match<B, A, C = B>(onNone: () => B, onSome: (a: A) => C): (o: Option<A>) => B | C
toNullable<A>(o: Option<A>): A | null
toUndefined<A>(o: Option<A>): A | undefined
toResult<E>(defaultError: E): <A>(o: Option<A>) => Result<A, E>
isSome<A>(o: Option<A>): o is Some<A>
isNone<A>(o: Option<A>): o is None
import { pipe, O } from '@stopcock/fp'
pipe(
O.fromNullable(config.port),
O.map(p => parseInt(p, 10)),
O.filter(p => p > 0 && p < 65536),
O.getOrElse(() => 3000),
)
pipe(
O.fromNullable(address),
O.flatMap(a => O.fromNullable(a.zip)),
O.match(() => 'no zip', zip => zip),
)

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>
ok<A>(value: A): Result<A, never>
err<E>(error: E): Result<never, E>
tryCatch<A>(thunk: () => A): Result<A, unknown>
fromNullable<E>(defaultError: E): <A>(value: A | null | undefined) => Result<NonNullable<A>, E>

tryCatch wraps a function that might throw. The error type comes back as unknown because JS can throw anything, so you’ll need to narrow it yourself.

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>
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>
getOrElse<B>(onErr: () => B): <A, E>(r: Result<A, E>) => A | B
match<E, B, A, C = B>(onErr: (e: E) => B, onOk: (a: A) => C): (r: Result<A, E>) => B | C
toOption<A, E>(r: Result<A, E>): Option<A>
isOk<A, E>(r: Result<A, E>): r is Ok<A>
isErr<A, E>(r: Result<A, E>): r is Err<E>
import { pipe, R } from '@stopcock/fp'
const parsed = R.tryCatch(() => JSON.parse(input))
pipe(
parsed,
R.map((obj: { value: number }) => obj.value),
R.flatMap(v => v > 0 ? R.ok(v) : R.err('must be positive')),
R.match(
err => console.error(err),
val => console.log(val),
),
)

// Option → Result
pipe(O.fromNullable(x), O.toResult('was null'))
// Result → Option (discards error)
pipe(R.ok('hello'), R.toOption) // Some('hello')
pipe(R.err('boom'), R.toOption) // None