Option Monad — Digitteck
Option Monad
frontend·8 June 2022·3 min read

Option Monad

According to Wikipedia, a monad is a design pattern that combines functions and wraps their return types in a wrapped type with additional computation. In practical terms: take a type T, and create a pipeline of chained functions that perform computation on an argument of type T, where each function takes into account the state returned by the previous call.

Three components make up a monad:

  • A wrapper type
  • A wrap function
  • A run function
Monad pipeline diagram

The requirements that drove this implementation:

  • Access nested properties without null-checking at each step
  • Each chained step can return a different T in Option<T> since we're traversing properties

The implementation:

  • A simple object containing the wrapped value, the wrap function, and the run function
  • Run returns the same T; RunTransform returns a new TNew
  • ValueOrNull and ValueOr are convenience methods beyond the formal monad pattern
typescript
export class Option<T> {
  private readonly _value: T | null | undefined;
  private readonly _hasValue: boolean;

  public get Value(): T {
    if (!this._hasValue) {
      throw `This option does not have a value`;
    }
    return this._value as T;
  }

  public get HasValue(): boolean {
    return this._hasValue;
  }

  constructor(value: T | null | undefined) {
    if (value === null || value === undefined) {
      this._hasValue = false;
      this._value = null;
    } else {
      this._value = value;
      this._hasValue = true;
    }
  }

  public static From<T>(value: T | null | undefined): Option<T> {
    return new Option<T>(value);
  }

  public Run(execute: (value: T) => Option<T>): Option<T> {
    return OptionRun(this, execute);
  }

  public RunTransform<TNew>(execute: (value: T) => Option<TNew> | TNew): Option<TNew> {
    return OptionRunTransform<T, TNew>(this, tr => {
      const res = execute(tr);
      if (res instanceof Option) {
        return res as Option<TNew>;
      } else {
        return Option.From(res as TNew);
      }
    });
  }

  public ValueOr(defaultValue: T): T {
    return this._hasValue ? this.Value : defaultValue;
  }

  public ValueOrNull(): T | null {
    return this._hasValue ? this.Value : null;
  }
}

export function OptionRun<T>(input: Option<T>, functor: (value: T) => Option<T>): Option<T> {
  if (input.HasValue) {
    return functor(input.Value);
  }
  return input;
}

export function OptionRunTransform<T, TNew>(
  input: Option<T>,
  functor: (value: T) => Option<TNew>
): Option<TNew> {
  if (input.HasValue) {
    return functor(input.Value);
  }
  return Option.From<TNew>(null);
}

Usage

With the Option monad:

typescript
const name = Option.From(activity)
  .RunTransform(r => r.ActivityTypeNode)
  .RunTransform(r => r.ActivityType)
  .RunTransform(r => r.Name)
  .ValueOr('');

Without:

typescript
const name = (activity
  && activity.ActivityTypeNode
  && activity.ActivityTypeNode.ActivityType
  && activity.ActivityTypeNode.ActivityType.Name)
    ? activity.ActivityTypeNode.ActivityType.Name
    : '';

Tags

TypeScriptMonadFunctional
digitteck

© 2026 Digitteck