TypeScript Maybe Type and Module
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
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" Nothing
s 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!