Reducing Null Errors with the Maybe Monad

Dealing with undefined or null values is an issue that can range from being a mild inconvenience to a giant source of bugs and regret. Today, I’m going to talk about a relatively elegant way to deal with undefined and null values: the Maybe monad.

Goals

My plan with this post is to avoid the famously difficult task of explaining monads, and instead focus on practical uses of the Maybe monad. I’m a big fan of TypeScript, so I’ll be using that, but the general idea is certainly not dependent on language.

I should also note that I’ve seen a number of implementations of the Maybe monad. I’ll be using my own version that may stray a bit from the purist’s definition, but it solves the problem.

The Problem

Just about every programmer has run into problems with null values at some point; we’ve all seen output in the form of NullPointerExceptions or undefined is not a function. At a very high level, we can say that null and undefined, which I’ll collectively refer to as Nil, represent an absence of value.

To illustrate my thinking, I’m going to use the following type definition throughout this post:


  type ChewToy: {
    type: string
  }
  type Pet: {
    name: string
    chewToy: ChewToy
  }
  type Person = {
    pet: Pet
  };

Let’s say we want to get a person’s pet’s name. We could write a function like this:


  const getChewToy = (p: Person) => p.pet.name

If the person doesn’t have a pet, or their pet doesn’t have any chew toys, we’re going to get an exception. And it’s probably pretty reasonable to imagine that in our system, there are people who don’t have a pet.

Okay, so we can just ensure that the values exist.


  const getChewToy = (p: Person) => p.pet && p.pet.name

This isn’t too bad, but it gets uglier when we have more deeply nested properties (like the type of the pet’s chew toy).

Maybe

Another approach is to create a type that wraps around these optional values and abstracts out the tedious parts—a Maybe type. I put the implementation at the bottom for reference, but I’m more intereseted in the usage.

Here’s an example:


  const chewToyType = new Maybe(person)  // Maybe
    .map(p => p.pet)                     // Maybe
    .map(pet => pet.chewToy)             // Maybe
    .map(chewToy => chewToy.type)        // Maybe
    .value();                            // string | undefined

The main thing here is the map method.


  map<U>(fn: ((x: T) => U)): Maybe<U>

It takes a function that maps the current value (of type T) to another value (of type U), if it exists. Whether or not the wrapped value exists or is Nil, we are returned a new Maybe that wraps something of type U or Nil. After the constructor is called, the returned type is Maybe, and after the first call to map, we have an object of type Maybe. This continues with each call to map, until we “unwrap” the value by calling value.

So what exactly happens if person is Nil when we call .map(p => p.pet)? The function that was passed in (the one that gets the pet from a person) isn’t called. Instead, it just returns a new Maybe with a wrapped value of Nil. This is chained through each call to map, and the value of chewToyType will end up being undefined. You don’t run the risk of seeing TypeError: Cannot read property 'pet' of undefined, or any other error types because the Maybe type handles this check for you.

If we want to ensure that chewToyType is never Nil, we could handle the Nil case by using valueOr like this:


  const chewToyType = new Maybe(person)  // Maybe
    .map(p => p.pet)                     // Maybe
    .map(pet => pet.chewToy)             // Maybe
    .map(chewToy => chewToy.type)        // Maybe
    .valueOr('none!');                   // string

Use Cases

Using the Maybe monad can be really nice when working with deeply-nested objects where null values are likely, as shown above.

I’ve found that I’ve gotten the most mileage out of the Maybe monad when storing it in Redux state for optional pieces of state. For example, let’s say your app has state with some optional properties that look like this:


  type State = {
    user?: Person
    modal?: {
      title: string
    }
  }

You could write it using Maybe types instead:


  type State = {
    user: Maybe<Person>
    modal: Maybe<{
      title: string
    }>
  }

Now you can write a user reducer like this:


  const userReducer = (userState: Maybe<Person>, action: UserAction) =>
    userState.map(user => {
      // if we get here, we know user is not nil
      // if don't get here, the reducer will just return a Maybe<Person>
      switch (action.type) {
        case 'GET_PET':
          return {
            ...user,
            pet: action.pet
          };
        default:
          return user;
      }
    });

Appendix: The Maybe Code

Here’s a quick implementation I put together.


  type Nil = undefined | null;
  const isNil = (x: any): x is Nil =>
    x === null || x === undefined;

  export class Maybe<T> {
    private wrappedValue: T | Nil;

    constructor(value: T | Nil) {
      this.wrappedValue = value;
    }

    public map<U>(fn: ((x: T) => U)): Maybe<U> {
      if (!isNil(this.wrappedValue)) {
        return new Maybe(fn(this.wrappedValue));
      } else {
        return new Maybe<U>(undefined);
      }
    }

    public value(): T | undefined {
      return this.wrappedValue;
    }

    public valueOr<U>(backupValue: U): T | U {
      if (!isNil(this.wrappedValue)) {
        return this.wrappedValue;
      } else {
        return backupValue;
      }
    }
  }

I chose here to create a Nil type so that we can handle null and undefined interchangably.