Skip to main content

result

Compile-time Result and Option mirroring Rust's monads.

import type {
  Ok, Err, Result,
  Some, None, Option,
  IsOk, IsErr, IsSome, IsNone,
  UnwrapOk, UnwrapErr, UnwrapSome,
  MapResult, MapOption,
  OkOrElse, SomeOrElse,
} from '@gentleduck/ttest/result'

Type-level discriminated unions modeling Result<T, E> and Option<T> from Rust. The shapes are designed to discriminate on ok / some — see discriminated for the broader pattern.

Ok

interface Ok<T> {
  readonly ok: true
  readonly value: T
}

The success branch of Result. Always ok: truetrue is a literal, not boolean, so the union is discriminable.

const v: Ok<number> = { ok: true, value: 1 }

Err

interface Err<E> {
  readonly ok: false
  readonly error: E
}

The failure branch of Result.

const e: Err<string> = { ok: false, error: 'parse failed' }

Result

type Result<T, E> = Ok<T> | Err<E>

A computation that either succeeds with T or fails with E. Narrow on r.ok.

function parse(s: string): Result<number, string> {
  const n = Number(s)
  return Number.isNaN(n) ? { ok: false, error: 'NaN' } : { ok: true, value: n }
}

const r = parse('42')
if (r.ok) r.value // number
else      r.error // string

Some

interface Some<T> {
  readonly some: true
  readonly value: T
}

The present branch of Option.

None

interface None {
  readonly some: false
}

The absent branch.

Option

type Option<T> = Some<T> | None

A value that may be absent. Use over T | null when you want the discriminator on a literal boolean rather than on nullishness.

function find<T>(xs: T[], p: (x: T) => boolean): Option<T> {
  const v = xs.find(p)
  return v === undefined ? { some: false } : { some: true, value: v }
}

IsOk

type IsOk<R> = R extends Ok<unknown> ? true : false

true if R is the Ok branch.

type a = IsOk<Ok<1>>      // true
type b = IsOk<Err<'x'>>   // false

IsErr

type IsErr<R> = R extends Err<unknown> ? true : false

true if R is the Err branch.

IsSome

type IsSome<O> = O extends Some<unknown> ? true : false

IsNone

type IsNone<O> = O extends None ? (O extends Some<unknown> ? false : true) : false

The inner Some check is required because Some<T> structurally extends None's some: false slot through union narrowing edge cases.

UnwrapOk

type UnwrapOk<R> = R extends Ok<infer T> ? T : never

Extract the value from an Ok branch, or never if R is not Ok.

type R = Result<number, string>
type V = UnwrapOk<R>   // number
type N = UnwrapOk<Err<'fail'>>  // never

UnwrapErr

type UnwrapErr<R> = R extends Err<infer E> ? E : never
type E = UnwrapErr<Result<number, string>>  // string

UnwrapSome

type UnwrapSome<O> = O extends Some<infer T> ? T : never
type V = UnwrapSome<Option<number>>  // number

MapResult

type MapResult<R, F>

Map the Ok value of R through Apply<F, X> (an HKT — see fp).

import type { Fn } from '@gentleduck/ttest/fp'

interface ToString extends Fn { return: `${this['arg'] & number}` }

type R = MapResult<Ok<42>, ToString>
//   ^? Ok<'42'>

type E = MapResult<Err<'fail'>, ToString>
//   ^? Err<'fail'>   // Err passes through unchanged

MapOption

type MapOption<O, F>

Map the Some value of O through Apply<F, X>. None passes through unchanged.

OkOrElse

type OkOrElse<R, Default> = R extends Ok<infer T> ? T : Default

Recover from Err with a default.

type V = OkOrElse<Err<'fail'>, 0>      // 0
type W = OkOrElse<Ok<42>, 0>           // 42

SomeOrElse

type SomeOrElse<O, Default> = O extends Some<infer T> ? T : Default

Recover from None with a default.

type V = SomeOrElse<None, null>             // null
type W = SomeOrElse<Some<'hi'>, null>       // 'hi'

Edge case: MapResult and MapOption are HKT-driven — the second generic is a Fn interface from fp, not a TypeScript function type. Pass an interface whose return projection consumes this['arg'].