Implementing a Maybe Pattern using a TypeScript Type Guard

Josh Wulf
Josh Wulf
Feb 12, 2019 · 5 min read
Image for post
Image for post
“I’m sorry sir, I couldn’t bring your beer from the kitchen…”

You’re sat at a restaurant, and you order a beer. The waiter disappears into the kitchen, and returns a couple of minutes later, empty-handed. “I’m sorry,” he says. “I could not get your beer.

Now here is the six-million dollar question: is the restaurant out of the beer that you ordered (business logic), or is the restaurant kitchen on fire (infrastructure failure)?

And how do you model those two distinct failure states in your API? (Credit to Kory Nunn for the restaurant metaphor).

Recently, I was working on a database library in TypeScript. It provides a strongly-typed abstraction over stored procedures to allow the application to declaratively interact with the data model without leaking the implementation details.

In this way we can implement caching, and even switch out the database without any change to the application logic or unit tests — which are easy to write because we can mock the data layer interface.

I took inspiration for this approach from a workshop I attended with Mark Seeman at NDC Sydney this year. In that workshop, we looked at modelling multiple return states using monoids.

Success and Failure: not a Binary State

These can be thought of as a failure mode (infrastructure failure), a success mode, and a mixed success/failure mode (infrastructure success / application data failure).

Kory Nunn describes the issue of the nuanced states like this: I go to a restaurant and order a beer. The waiter goes to the kitchen and then comes back and says: “I’m sorry, I couldn’t get you a beer.

The issue here is: Why couldn’t you get me a beer? Are you out of beer (no record found)? Or is the kitchen on fire (database disappeared)?

This is a collapse between the two possible failure modes.

Kory has developed the Righto library for lazily evaluating asynchronous operations and modelling these two failure states.

For this library, however, we took a different approach, taking inspiration from the Maybe monad.

The Kitchen Is Definitely On Fire!

If the kitchen is not on fire, however, we need to model two states: we got a record back (“here is your beer!”), or there was no matching record found (“sorry, we’re all out of Fosters lager. Actually, we don’t stock it in Australia.”).

A simple way to do this is to test the return from the database call to see if it is null. However, this cannot be checked statically, as it relies on the runtime return value. We also can’t enforce that a developer handle this case (with one caveat that I will address).

The Kitchen Isn’t On Fire — This May Be Your Beer, Or Not…

We can use TypeScript’s type guards to enforce handling both cases. There are TypeScript Maybe Monad implementations, like TS-Monad. However, these burden developers with a verbose syntax that obscures the code.

As an example, here is some idiomatic JavaScript where the developer tests for a value before attempting to use it:

if (age) {
var busPass = getBusPass(age); // might be null or undefined
if (busPass) {
canRideForFree = busPass.isValidForRoute('Weston');
}
}

Here’s my caveat from earlier: if you use strict null checking and define the return signature of getBussPass() as BussPass? or BussPass | undefined, then you will get an error that busPass may be undefined. You can use a type guard on this, but your intent is not explicit.

With the TS Monad, you write this as:

var canRideForFree = user.getAge() 
.bind(age => getBusPass(age))
.caseOf({
just: busPass => busPass.isValidForRoute('Weston'),
nothing: () => false
});

It’s explicit, but verbose — and not idiomatic JavaScript or TypeScript. Not ideal.

Using the TypeScript Type Guard to Type “Maybe”

We defined the signature of our method as MaybeRecord where:

type MaybeRecord = Record | RecordNotFound

We then define the two types like this:

class RecordNotFound {
found: false = false;
}
class Record {
found: true = true;
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}

In the method implementation we return either new RecordNotFound() or new Record(res.name, res.age).

Then in the consuming code, the return value has only the intersection property found. So if we make a small test implementation:

function returnMaybeRecord(exists: boolean): MaybeRecord {
if (exists) {
return new Record('Joe Bloggs', 42);
} else {
return new RecordNotFound();
}
}

We can see what happens in VSCode when we do this:

const maybeExists = Math.random() > 0.5;
const maybeRecord = returnMaybeRecord(maybeExists);
Image for post
Image for post

The only property that reliably exists on the returned value is found. If we now write a type guard:

if (maybeRecord.found) {}

And now, inside the type guard we have a Record:

Image for post
Image for post

This enforces in the application that the maybeRecord must be explicitly checked before it can be used.

About Me: Software engineer at Credit Sense, Red Hat Alumnus, founder of magikcraft.io. I live in Brisbane, Australia.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store