TypeScript Maybe Type and Module

Kyle Corbelli
DSC Engineering
Published in
5 min readMay 2, 2018

--

If you’ve ever written software in a type-safe functional programming language you’ve likely developed an affinity for, or at least a familiarity with, the Maybe type. You might know it as the option or Optional type. Regardless of what you call it, its absence in JavaScript leaves you feeling exposed to null and undefined runtime bugs. Since TypeScript doesn’t have a built-in Maybe type, we’ll create a simple TypeScript Maybe type and corresponding module of utility functions in this post.

Table of Contents

Just gimme the code

1. The Problem
2. The TypeScript Maybe Type
3. Why Go Through All This Trouble?
4. The TypeScript Maybe Module

The Problem

Maybe lets you define a value within a context of possible absence/nonexistence. What kind of absence or nonexistence? Some examples include accessing the possibly null/undefined value associated with a particular key in a dictionary/hashmap. The key/val pair may not exist at all. You’d then be working with a value you expect to be defined but isn’t!

Another example is division by zero. No number may be divided by zero and so this results in a nonexistent value. And to add insult to injury, JavaScript gives you 1 / 0 === Infinity and 0 / 0 === NaN. Wat?!

These are just two examples that will work their way through your software and eventually result in runtime bugs, software failure and confused/upset users. Eeek!

So how do we fix this? We’ll create and use a TypeScript Maybe type to let us and the compiler know a particular value may be defined or be absent. Now the compiler will let us know whenever we try to do something silly with a possibly undefined/null value.

The TypeScript Maybe Type

Let’s define the Maybe<T> type in TypeScript as the discriminant union of Nothing and Just<T> interfaces:

The last eight lines just give us some convenience functions that act as value constructors that create values of type Nothing or Just .

What’s up with the MaybeType enum? In order to get all the nice type safety of type guards when switching on a union type, that union type needs to have a "discriminant property", which has a concrete and unique value defined for each of its composite types. Here, an instance of Nothing needs to have type: MaybeType.Nothing and an instance of Just<T> needs to have type: MaybeType.Just. This is how TypeScript knows what kind of instance it is working with while inside of a particular case statement in a switch block. For example, let’s say you have a function that takes a possibly absent name: string and returns a greeting string:

Now, within case MaybeType.Nothing:, TypeScript knows that maybeName doesn't have a .value property. Similarly, within case MaybeType.Just:, TypeScript knows maybeName has a string-typed .value property. Type guards and type safety, plus TypeScript will warn you if you don’t consider either case!

It sure would be nice to just pattern match on the actual type itself and unwrap the value “inside” the Just<T> instance in the case statement like we can in Elm, Haskell or ReasonML, but this gets the job done even if it isn’t quite as pretty.

Why Go Through All This Trouble?

Why not just use null checks and undefined checks everywhere? We certainly could do this for some cases. But what if we’re defining a function using function composition and somewhere along the way one of the functions we’re composing returns a null or undefined? The next function in that composition chain might just blow up because it’s expecting a defined input of a certain type.

For example, the head function in some languages returns the first element of a list/array and will error if given an empty list. In JavaScript, trying to access list[0] when const list = [] returns undefined. In the following example we define an unsafeHead function that blindly tries to access the first element of a list/array and we then compose it with Ramda's toUpper, which simply makes a string uppercase:

Everything blew up because toUpper expects a string input and plans to call the String.prototype.toUpperCase method on it. Now, when we really think about the design of our little program, it is more accurate that a head function return a Maybe<T> because it might not be defined at all in the case of an empty list. We also want to handle the composition chain failure more elegantly. We could check for null or undefined inside of toUpper's function definition but what if we don’t have access to that code? Or what if this is such a common pattern that we don’t want to perform null/undefined checks in every one of our functions that follows a potential absence? First, let’s rework our unsafeHead function into a safeHead function:

The compiler now knows that we may experience an absence and will make sure we handle it. If the list is empty, we get an instance of Nothing; otherwise, we get an instance of Just<T>. Without diving even a little bit into theory, the Maybe type and module in languages like Haskell provide many useful utility functions for handling situations exactly like this, where we have a value within the context of possible absence. One such function is map, which is a way to apply functions to the possible value contained "inside" a Maybe instance. Given two parameters: (1) a function that transforms a value of type A into a value of type B, and (2) a Maybe<A>, it returns a Maybe<B>. If we give map some function and a Nothing, it just returns a Nothing. And if we give map some function and a Just<A> it will apply the function to the "wrapped" value inside the Just and return to us a Just<B>. Here is an example implementation:

On line 8 we see that the provided function is applied to the “wrapped” value inside the Just instance if that’s what the provided Maybe happens to be. Otherwise, it just returns a Nothing. We’ll namespace our export as a Maybe module, not to be confused with the Maybe<T> type. Now we can rework the composition chain of upperCaseHead to account for this potential absence:

Now if we experience an absence the composition chain continues right along without blowing up and in the end we simply receive a Nothing. And this is indeed a more accurate representation of the program since there might in fact be no head to uppercase! Line 4 returns a Maybe<string> and since toUpper expects a bare string, we pass toUpper to the curried Maybe.map function on line 3 so that it can be conditionally applied to the wrapped string inside the Just<string> in the case of success or be ignored in the case of Nothing resulting from safeHead. In this same fashion, we can add more functions to this composition chain and if handled properly with Maybe.map the entire chain will either succeed or will "short circuit" on the first absence and just keep "passing along" Nothings without blowing up with an exception and without having to mess with the underlying implementation of each function in the chain.

The TypeScript Maybe Module

There are, of course, many other applications of the Maybe type and module, many more utility functions, and I’d encourage you to explore them in languages like Elm and Haskell. For now, here is a more complete TypeScript Maybe module that mostly mirrors the functions available in Elm:

Check out the GitHub repo here.

So what do you think about the TypeScript Maybe? Let me know by dropping a comment!

--

--