hand sketched logo of electrons orbiting a nucleus

Why JavaScript Isn't Enough: Undefined

Why are we wrapping our code in more and more tools if at the end of the day it all just runs in JavaScript -- A Colleague

This is a fair question. The goal at the end of the day for any engineer is to execute work that brings the business value. And we hope to do with mastery, autonomy, and purpose. [Pink 2009]

Simple User

You can follow along with the code here: https://codesandbox.io/s/why-javascript-isnt-enough-0q783

Let's say we have a user. That user has a name, sometimes We'll ignore where we got his user for this post.

const jane = {
  name: 'jane',
};

const noName = {
  name: undefined,
};

Alright, let's write a function to say hello.

const sayHello = (name) => `hello, ${name}`;

And use that function:

const log = console.log;

log(sayHello(jane.name)); // 'hello, jane'

log(sayHello(noName.name)); // 'hello, undefined'

Well, thats about what we'd expect.

Dont Print Undefined

Say we don't want to print out 'hello, undefined' if the user doesn't have a name.

One way to do this is to add some protections to our sayHello function.

const sayHelloProtected = (name) => {
  if (name === undefined) {
    return 'cant say hello without a name';
  }

  return `hello, ${name}`;
};

Let's see how that works:

log(sayHelloProtected(jane.name)); // 'hello, jane'

log(sayHelloProtected(noName.name)); // 'can't say hello without a name'

Progress! Here is where a lot of us hop back into work and go along. We can go a long way with this level of programming.

At some point we might experience a moment of friction. We get an error where undefined crashes production. We get a user complaint about a UI that has undefined displaying to them. We might annoyed that we need to keep writing guards to protect against possibly undefined values in our code. We might get annoyed that we are now writing unit tests to check if we handle undefined well, everywhere.

One or any of these friction points might cause us to ask, is this the best we can do?

Typing types

We hear people talking about TypeScript, we hear that popular libraries are starting to be written in TypeScript. Is it hype? Or does it have staying power?

Let's add some types to our code. We're going to start back with our original sayHello function and add a type on the input:

const sayHelloTyped = (name: string) => `hello, ${name}`;

Ok, that wasn't too crazy. We told the TypeScript compiler that our function sayHelloTyped takes an argument that is a string.

Now we can tell the TypeScript compiler about the shapes that our user objects can take:

type User = {
  name: string | undefined;
};

Here we've said that a User has a field with key name and the value of that field is either a string or undefined.

Now we can tell the compiler that our users are of this type:

const jane2: User = {
  name: 'jane',
};

const noName2: User = {
  name: undefined,
};

If you open codesandbox or setup your editor and write

log(sayHelloTyped(jane2.name));

We'll get red swiggles as the compiler has predicted an error that could happen!

Wonderful! We have the computer thinking ahead for us and letting us know that we need to make sure to handle the case where the User name is undefined. Let's do that:

const sayHelloTyped2 = (name: string | undefined) => `hello, ${name}`;

Lets see what our editor thinks about that with noName:

Uh oh. No red swiggles, TypeScript is happy, but we are about to get our 'hello, undefined' error back. Template literals are typed to allow undefined.

This is actually an open issue on the TypeScript project: https://github.com/microsoft/TypeScript/issues/30239

Even if TypeScript was updated so that template literals didn't allow undefined we'd still run into the problem, that our colleague pointed out, where our code compiles down to JavaScript stripping away any type safety.

What are we to do?

We've got Options

Luckily we're not the first people to run into this issue. From functional programming world there is a thing that can describe to the compiler and in runtime our situation.

In this post we are going to use a library called fp-ts. It contains many useful modules for writing in typed functional programming style. Today we are going look at one module.

Welcome to the Option module and type. You can think of the option as an object that either has something or has nothing. If we describe our User with this we'd write:

import * as O from 'fp-ts/lib/Option';

type UserSafe = {
  name: O.Option<string>;
};

Here we've said that the name field has a value of option that contains a string.

Let's constructor our users to match this type:

const janeSafe: UserSafe = {
  name: O.some('jane')
};

const noFaceSafe: UserSafe = {
  name: O.none
};

Now if write out our usual log, we'll get a red swiggle type error:

Now how do we deal with this? One way is teaching sayHelloTyped how to handle options. But this mixes concerns. This function is beautiful and simple right now. Another way is to create a new function that uses sayHelloTyped.

We're gonna bust out some functional programming here that we've not introduced yet. We can read through this and still get a sense of what is happening:

import { pipe } from 'fp-ts/lib/function';

const sayHelloSafely = (name: OptionT<string>) =>
  pipe(
    name,
    Option.map(sayHelloTyped),
    Option.getOrElse(() => 'cant say hello without a name'),
  );

Let's talk through sayHelloSafely. This function takes in a name that is now an option that can contain a string.

Whats this pipe thing? It similar to the "pipe" in bash written as |. It allows us stack functions where the output of one becomes the input of another.

Its hard to read but here is what sayHelloSafely would look like if we wrote it without the pipe:

const sayHelloSafely2 = (name: O.Option<string>) =>
  O.getOrElse(() => 'cant say hello without a name')(
    O.map(sayHelloTyped)(name),
  );

What's this O.map? This lets reuse any function doesn't take option and use it as if it did.

What's this O.getOrElse? This lets us handle the case where there is no name in a way that is separate from our original sayHelloTyped function.

All together now, we get: https://codesandbox.io/s/why-javascript-isnt-enough-0q783

const janeSafe: UserSafe = {
  name: O.some('jane'),
};

const noFaceSafe: UserSafe = {
  name: O.none,
};

const sayHelloTyped = (name: string) => `hello, ${name}`;

const sayHelloSafely = (name: O.Option<string>) =>
  pipe(
    name,
    O.map(sayHelloTyped),
    O.getOrElse(() => 'cant say hello without a name'),
  );

log(sayHelloSafely(janeSafe.name)); // 'hello, jane'

log(sayHelloSafely(noFaceSafe.name)); // 'cant say hello without a name'

Kinda weird, kinda cool. We've taught the JavaScript runtime a new trick. Before we expressed "none" via undefined or null. Now we express "none" via Option.none. And we have these functional programming legos of pipe, Option.map, Option.getOrElse to allow us to handle non-option aware code to use options.

How does this get us closer to our goal?

The goal at the end of the day for any engineer is to execute work that brings the business value. And we hope to do with mastery, autonomy, and purpose. [Pink 2009]

Bugs are anti-value. Types and functional programming reduce bugs by forcing errors from runtime to compile time.

Business changes fast which requires software changes. When software changes we have more opportunities for bugs. Types and functional programming allow your editor and build tools to tell you if that change is gonna break anything else both it happens in runtime.

The mind can only think about so much at once. Separating concerns into bite-size pieces allows us to think clearly. Once those bite-size pieces are clear, we can lean on types and functional programming to combine them in a way that will ensure they continue to work as expected.

JavaScript runs where our users are. Types and functional programming allows us to write programs we are more sure will run the way we want for our users.

References

Pink, Daniel. 2009. Drive: The Surprising Truth About What Motivates Us

Critiques

I love working with fp-ts BUT I think the jump into it toward the end a jump higher in pace that is out of place with the beginning of the post. If I make time, I'll try to refactor this post to write its own Option and not use any outside libraries.