How is the Maybe monad useful in TypeScript?

Viewed 760

I’m struggling to understand the value of having a Maybe type in TypeScript. I’m trying to make sense of the code example from this article about implementing your own Maybe type in TypeScript:

enum MaybeType {
  Just = 'maybe-type__just',
  Nothing = 'maybe-type__nothing',
}

interface Just<T> {
  type: typeof MaybeType.Just
  value: T
}

interface Nothing {
  type: typeof MaybeType.Nothing
}

type Maybe<T>
  = Just<T>
  | Nothing

const Nothing = (): Nothing => ({
  type: MaybeType.Nothing,
})

const Just = <T> (value: T): Just<T> => ({
  type: MaybeType.Just,
  value,
})

How is this actually useful? I don’t understand how it’s any better than a type just being T | undefined.

My (very limited) understanding is the Maybe type can free you from certain runtime errors and eliminate the need for null/undefined checks throughout your code. But aren’t you just trading having undefined checks littered throughout your code for havingNothing checks littered throughout your code?

In this example, a Nothing result has no value property, but a Just type does. So TypeScript won’t even let me access that value without first making sure that the result is indeed a Just. So doesn’t that mean lots of checks everywhere? Why not just keep doing plain-old undefined checks? Is the example that I’m looking at just a crummy implementation of the Maybe type?

Clearly I’m missing something and there’s more to it. How does this thing work and how is it useful?

1 Answers

You're correct in that the code snippet you've included offers very little tangible value above T | undefined.

However later on in the linked article the author introduces additional functions such as maybeMap and this is where the value lies:

function maybeMap<A, B> (f: (val: A) => B, m: Maybe<A>): Maybe<B> {
  switch (m.type) {
    case MaybeType.Nothing:
      return Nothing()
    case MaybeType.Just:
      return Just(f(m.value))
  }
}

Consider this situation:

type Person = {
  name: string;
  age: Maybe<number>; // We don't always know their age
}

const bob:Person = {
  name: "Bobby Tables",
  age: Just(23)
}

Now imagine we have a people-finding system with the following method:

   function findByName(personName: string): Maybe<Person>

This nicely represents the fact that we might not find a suitable person, but we're still not getting much value over Person | undefined.

But now imagine we need to obtain the age of the person we find.

Because Maybe<T> is monadic, we can compose things to avoid littering our code with checks for undefined:

  function findAgeOfPerson(personName: string): Maybe<number> {
    const maybePerson = findByName(personName);
    return maybePerson.map((p) => p.age);
  }

No if statements, no truthiness checks.

If maybePerson is Nothing then map just returns Nothing, but if not then (p) => p.age does exactly what we need in complete safety.

Related