Functional Type Safety in Javascript with Maybe

devinholloway profile image Devin Holloway ・6 min read

In programming languages (more so functional programming languages) and type theory, an option type or maybe type is a polymorphic type that represents encapsulation of an optional value; e.g., it is used as the return type of functions which may or may not return a meaningful value when they are applied. It consists of a constructor which either is empty (often named None or Nothing), or which encapsulates the original data type A (often written Just A or Some A).
-- Wikipedia

Let's talk about what this means, why it's useful, and how to utilize the concept in Javascript.

Javascript is an untyped language, which makes it very flexible, and in some cases, very powerful. But with that power comes great responsibility. Take for instance, a function designed to operate on a string:

const capitalize = a => a.charAt(0).toUpperCase() + a.slice(1)

capitalize('javascript') //=> "Javascript"

Now substitute the string for any other datatype:

capitalize(5) //=> a.charAt is not a function
capitalize(true) //=> a.charAt is not a function
capitalize(['javascript']) //=> a.charAt is not a function
capitalize(null) //=> Cannot read property 'charAt' of null
capitalize(undefined) //=> Cannot read property 'charAt' of undefined

Anyone who's done a fair amount of Javascript will recognize that mismatched datatypes and null/undefined are a common source of run-time errors. There are, of course, various ways to write safer functions, often referred to as defensive programming:

const capitalize = a => (typeof a === 'string') 
  ? a.charAt(0).toUpperCase() + a.slice(1) : ''

While this is a much safer version, it can add a lot of code cruft, especially when you need these types of checks scattered all throughout your code base. In addition, it forces you to think (and therefore write) in a more imperative way, rather than a more expressive way that functional programming promotes.

The way we deal with null/undefined or type mismatches depends on whether they should be expected and whether the data can be controlled. For example, if we want to capitalize each part of a person's name, and the middle name isn't a requirement in our data, we can expect it to be unavailable (or null) when given to a function. In that case, we'd, ideally, prefer to just skip the function call and let the rest of code continue its execution. This is one of the benefits we get from the Maybe data type.

A Maybe is a Sum Type that can represent one of two other types; a Just or Nothing (or a Some/None, depending on the language). You can think of it as a polymorphic relationship where Just represents a correct or valid value, and Nothing represents an incorrect, invalid, or lack of value (such a null).

Both Just and Nothing act as a container, or wrapper, for raw data. The significance of this is that functions that know how to work with a Maybe can also work with Just or Nothing, even if the raw data is invalid. Each of these wrappers have the same API, allowing them to be interchangeable.

This isn't so different from the way Javascript primitives work. When you execute code such as 'javascript'.toUpperCase(), it's not the string, itself, that has the toUpperCase() function attached to it. After all, string is a primitive, meaning it has no functions or properties. Instead, it's the String() constructor that has the toUpperCase() function, and Javascript will auto-wrap the primitive when calling contructor fuctions/properties on it.

Let's look at some actual code. For the examples in this article we'll be using the Crocks library.

There are multiple ways to construct a Maybe data type, such as using the Maybe constructor itself:

const Maybe = require('crocks/Maybe')

Maybe('javascript') //=> Just "javascript"
Maybe.of('functional') //=> Just "functional"
Maybe.of(null) //=> Just null

The Maybe constructor will always produce a Just. It's recommended to use the Just and Nothing constructors directly, if only for readability:

Maybe.Just() //=> Just undefined
Maybe.Just('javascript') //=> Just "javascript"
Maybe.Nothing() //=> Nothing
Maybe.Nothing('javascript') //=> Nothing

You can also destructure Just and Nothing to tighten up your code:

const Maybe = require('crocks/Maybe')
const {Just, Nothing} = Maybe

Just() //=> Just undefined
Nothing() //=> Nothing

But most of your Maybe types will be produced from helper functions. The focus of this article will be on the safe helper function.

safe takes a predicate function, which returns a boolean, and a value to be applied to the predicate. If the predicate returns true, we get a Just, otherwise, a Nothing:

const Maybe = require('crocks/Maybe')
const safe = require('crocks/Maybe/safe')

const isString = a => (typeof a === 'string') 

safe(isString, 'javascript') //=> Just "javascript"
safe(isString, 5) //=> Nothing
safe(isString, null) //=> Nothing

Safe is curried, allowing us to preconfigure it with a predicate and pass in the data later. For brevity, we'll also switch to Crock's builtin isString function:

const Maybe = require('crocks/Maybe')
const safe = require('crocks/Maybe/safe')
const isString = require('crocks/predicates/isString')

const safeString = safe(isString)

safeString('javascript') //=> Just "javascript"
safeString(5) //=> Nothing

A Maybe (and therefore Just and Nothing) implements a wide range of algebraic structures, one of which is the Functor allowing us to map a Maybe.

One of the rules of a Functor is that when we map a value to another, we get back the same type and structure. If we map an array, we'll get back an array of the same size (with differing values). If we map a Maybe we'll get back a Maybe. We're only affecting the raw data inside. Let's go back to our original caplitalize function and map it to our Maybes:

const safeString = safe(isString)
const capitalize = a => a.charAt(0).toUpperCase() + a.slice(1)

safeString('javascript').map(capitalize) //=> Just "Javascript"
safeString(5).map(capitalize) //=> Nothing
safeString(null).map(capitalize) //=> Nothing

When we map a valid (Just) value, the mapping will unwrap the raw data from our Maybe, pass it into the mapper (capitalize), and re-wrap the result. When we try to map an invalid (Nothing) value, the mapper will be ignored and just return a new Nothing.

The thing to point out here is that our capitalize function is just a regular Javascript function without any type checks or null checks. In fact, we don't have any type/null checks anywhere in our code. That's all abstracted away in the Maybe type. Passing a safeString to capitalize is guaranteed to be error free.

Another thing I'll point out is that an invalid value doesn't have to be only values that produce an error. For example, an empty string could be safely passed to capitalize, but there'd be no point. If we rewrote our safeString function to exclude empty strings from being valid (and rename it to validString) we could avoid the performance cost of executing the capitalize function. This would become more value when implementing expensive operations such as making a service call.

Finally, there will come a time when you're ready to unwrap the raw data and discard the Maybe container. This would usually be at the end of the flow, such as rendering the value on screen, or passing it to a service method. This can be done with Maybe's option function:

safeString('javascript').map(capitalize).option('') //=> 'Javascript'
safeString(5).map(capitalize).option('') //=> ''
safeString(null).map(capitalize).option(null) //=> null

option takes a single parameter, a default value, to use when unwrapping a Nothing. When unwrapping a Just, the default is ignored and the raw data is returned. I would caution against unwrapping your data too early. There isn't anything that can be done to raw data that can't also be done to the same data when wrapped. I've shown an example of transforming wrapped data with map, but there's many more functional applications for wrapped data.

This was very much an introduction to type safety with Maybe. There are many more useful applications with Maybe as well as other structures to help write error free code in an expressive way. I'll be writing a future post on Either, a structure that allows you to work with errors (instead of just avoiding the execution of unsafe code) and eliminating the use of try/catch.

Posted on by:


markdown guide