writing monads in typescript

Posted on:October 31, 2023

Table of Contents

What is a Monad?

A monad is a functional programming concept that essentially allows you to encapsulate the result of a function, with logic pertaining to how different result case’s should be treated. It is a way of composing functions that allow you to write code that is more readable and maintainable, when properly understood. Having said that, it is not a concept that is easy to understand, and can result in programmers not knowing how their implementation works. Their appearance can create confusion by obfuscating logic to places which are not immediately obvious. Once mastered, they are a useful form of encapsulating tedious detials of computational patterns, into concise functions.

Monads generally are used to simplify a program’s strucure, relying on abstraction to improve separation of concerns throughout the program. These abstractions create a more declarative style of programming, which is more readable and maintainable. If it is possible to remain agnostic about operational details, while still working with different underlying types, then a monad is a good candidate for the job. Conceptually, a monad is a type of functor, which is a type that maps a function over a collection of values. Having a strong understanding of the following concepts will help you understand monads:

If you are unfamiliar with monads but have come across the above concepts, it is likely that you have used a monad without realizing it. Below are some examples of monads that are commonly used in various programming languages:

Creating Monad’s in Typescript

Below we will explore how to create some of the above monads in typescript. There are 3 main components to a monad:

  1. A monadic type: 0, 1 or many types that are wrapped in the monad
  2. A unit operation: a function that takes a value of the monadic type and returns a monad
  3. A bind function or composition opertion: a function to call on each unit in the monad

Some monads/monad-like operations commonly used in typescript:

  • Promise Monad
  • State Monad in frameworks using React.js
  • Array.prototype.flatMap() performs a monadic bind operation on an array of monadic types.

Maybe Monad

The maybe monad is an increasingly popular monad, as of late. I’ve personally heard numerous statements how great rust using is, and how it’s Option type is extremely useful for writing safe code. The Maybe monad (also known as the Option monad), is a monad that is used to represent a value that may or may not exist. In typescript, a very similiar pattern can be descrbed to the Maybe monad is when using the optional chaining operator (?.) on an object. Essentailly, the Maybe monad is a way of abstracting away the details of handling when something might exist. This comes in handy when sequential execution/function chained only happens when results are defined. Using the maybe monad can be seen in the following example:

type None = null | undefined;
const isNone = (value: any): value is None =>
  value === null || value === undefined;

class Maybe<T> {
  constructor(private value: T | None) {}

  static just<T>(value: T): Maybe<T> {
    if (isNone(value)) return Maybe.nothing<T>();
    return new Maybe<T>(value);
  }

  static nothing<T>(): Maybe<T> {
    return new Maybe<T>(null);
  }

  static from<T>(value: T): Maybe<T> {
    return Maybe.just(value);
  }

  public map<U>(f: (value: T) => U): Maybe<U> {
    if (isNone(this.value)) return Maybe.nothing<U>();
    return Maybe.just<U>(f(this.value));
  }

  public match<U>({
    just,
    nothing,
  }: {
    just: (value: T) => U;
    nothing: () => U;
  }): U {
    if (isNone(this.value)) return nothing();
    return just(this.value);
  }

  public flatMap<U>(f: (value: T) => Maybe<U>): Maybe<U> {
    if (isNone(this.value)) return Maybe.nothing<U>();
    return f(this.value);
  }
}

// Maybe Monad Example

/**
 * Output:
 * Maybe { value: 16 }
 */
const hasResult: Maybe<number> = Maybe.from(10)
  .map(x => x + 1)
  .map(x => x + 2)
  .map(x => x + 3);
console.log(hasResult);

/**
 * Output:
 * Maybe { value: null }
 */
const noResult: Maybe<number> = Maybe.from(10)
  .map(x => null)
  .map(x => x + 1);
console.log(noResult);

// Map vs FlatMap

/**
 * Output:
 * Maybe { value: Maybe { value: 2 } }
 */
console.log(Maybe.from(1).map(x => Maybe.from(x + 1)));
/**
 * Output:
 * Maybe { value: 2 }
 */
console.log(Maybe.from(1).flatMap(x => Maybe.from(x + 1)));

This is a very simple implementation of the Maybe monad, but is a great example about how can reduce the amount of boilerplate code that is required to write safe code. In this example, the maybe function is a simple function that takes a value and returns it. The maybeMap function is a function that takes a function and returns a function

Either Monad

The either monad is very similar to the maybe monad, the key difference being Either<E, A> requires two types to be defined. Whereas Maybe<T>, has two possible resulting types, but only requires one type in its definition. On a high-level, Either is just a more generic version of Maybe, where Maybe<T> is logically equivalent to Either<None, T>. In more detail, Either<E, A> buids on wrapping the types it is given, and using/matching the bind function to chain operations together. It can be used to represent computations that might fail. It usually has two types: Left (representing failure) and Right (representing success). Using the Either monad in the below example, requires specifing how to handle the Left and Right cases. This is done by using the bind function, which takes a function that returns an Either type.

export namespace EitherMonad {
  export type Left<E> = { kind: "left"; value: E };
  export type Right<A> = { kind: "right"; value: A };
  export type Either<E, A> = Left<E> | Right<A>;

  export function left<E>(value: E): Either<E, never> {
    return { kind: "left", value };
  }

  export function right<A>(value: A): Either<never, A> {
    return { kind: "right", value };
  }

  export function bind<E, A, B>(
    m: Either<E, A>,
    callbackfn: (_: A) => Either<E, B>
  ): Either<E, B> {
    if (m.kind === "left") {
      return m;
    }
    return callbackfn(m.value);
  }
}

// Either Monad Example
function divByZero(x: number, y: number): EitherMonad.Either<number, number> {
  if (y === 0) return EitherMonad.left(x);
  return EitherMonad.right(x / y);
}
/**
 * Output:
 * { kind: 'left', value: 10 }
 */
console.log(divByZero(10, 0));
console.log(EitherMonad.bind(divByZero(10, 0), x => divByZero(x, 2)));

/**
 * Output:
 * { kind: 'right', value: 5 }
 */
console.log(divByZero(10, 2));

This example is not directly equivalent to the Maybe monad example, and is shown to demonstrate a simplistic representation of the Either monad.

IO Monad

In layman terms, io monad is used to manage side effects on file operations (reading/writing/deleting). The core concept describing an IO<A> monad can be described as an action to be performed in the world, optionally providing information about the world of type A.

In a more technical sense, the IO<A> monad represents a non-deterministic synchronous computation that can cause side effects, yields a value of type A and never fails.

Reader Monad

Writer Monad

State Monad

This monad is used to model a single piece of muteable state S. The react hook useState is a very commmon example of a state monad. The useState hook sets the initial state of a variable, and returns a function that can be used to update the state. This is one method of implementing a state monad, but there are other ways to implement it.

List Monad

Promise Monad

Join the Newsletter!