Replace null with ES6 Symbols

When I was working on my small side-project library, I needed to represent a missing value. In the past, I'd used the nullable approach in simple settings and Option (aka Maybe) when I wanted more control.

In this case, neither felt correct so I came up with a different approach I'd like to present.

Why Nullable was not enough

Nullable means that when there is a value it is a string, a number, or an object. When there is no value, we use either null or undefined.

Tip: if you work with nullable types in TypeScript, make sure you turn on the strictNullChecks

This is often fine.

There are, in general, two cases when it's not:

  1. The value can be null or undefined. In the end, these are both valid JavaScript primitives and people can use them in many ways.

  2. You want to add some advanced logic. Writing x == null everywhere gets cumbersome.

In my case I was handling an output of a Promise, that can return
anything. And I could foresee that both of the ‘missing’ will be eventually returned.

In general, the problem 1 and 2 have the same solution: use a library that implements the Option type.

Why Option was too much

Option (sometimes called Maybe) type has two possibilities: either there is no value (None on Nothing) or there is a value (Some or Just).

In JavaScript/TypeScript this means introducing a new structure that wraps the value. Most commonly an object with a property tag that defines what possibility it is.

This is how you could quickly implement Option in TypeScript:

type Option<T> = { tag: 'none' } | { tag: 'some', value: T }

Usually, you would use a library that defines the type and a bunch of useful utils alongside. Here is an intro to Option in my favourite fp-ts library.

The library I was building was small, had zero dependencies, and there was no need for using any Option utility. Therefore, bringing in an Option library would be overkill.

GitHub logo robinpokorny / promise-throttle-all

🤏 Promise.all with limited concurrency

For a while I was thinking about inlining the Option, that is coding it from scratch. For my use case that would be just a few lines. It would complicate the logic of the library a bit, though.

Then, I had a better idea!

Symbol as the new null

Coming back to Nullable, the unsolvable problem is that null (or undefined) is global. It is one value equal to itself. It is the same for everybody.

If you return null and I return null, later, it is not possible to find out where the null comes from.

In other words, there is ever only one instance. To solve it, we need to have a new instance of null.

Sure, we could use an empty object. In JavaScript each object is a new instance that is not equal to any other object.

But hey, in ES6 we got a new primitive that does exactly that: Symbol. (Read some introduction to Symbols)

What I did was a new constant that represented a missing value, which was a symbol:

const None = Symbol(`None`)

Let's look at the benefits:

  • It is a simple value, no wrapper needed
  • Anything else is treated as data
  • It's a private None, the symbol cannot be recreated elsewhere
  • It has no meaning outside our code
  • The label makes debugging easier

That is great! Especially the first point allows using None as null. See some example use:

const isNone = (value: unknown) => x === None

const hasNone = (arr: Array<unknown>) =>
  arr.some((x) => x === None)

const map = <T, S>(
  fn: (x: T) => S,
  value: T | typeof None
) => {
  if (value === None) {
    return None
  } else {
    return fn(value)
  }
}

Symbols are almost nulls

There are some disadvantages, too.

First, which is IMO rare, is that the environment has to support ES6 Symbols. That means Node.js >=0.12 (not to be confused with v12).

Second, there are problems with (de)serialisation. Funnily, Symbols behave exactly like undefined.

JSON.stringify({ x: Symbol(), y: undefined })
// -> "{}"

JSON.stringify([Symbol(), undefined])
// -> "[null,null]"

So, the information about the instance is, of course, lost. Yet, since it then behaves like undefined—the native ‘missing value’)—makes it well suited for representing a custom ‘missing value’.

In contrast, Option is based on structure not instances. Any object with a property tag set to none is considered None. This allows for easier serialisation and deserialisation.

Summary

I'm rather happy with this pattern. It seems it's a safer alternative to null in places where no advanced operations on the property are needed.

Maybe, I'd avoid it if this custom symbol should leak outside of a module or a library.

I especially like that with the variable name and the symbol label, I can communicate the domain meaning of the missing value. In my small library it represents that the promise is not settled:

const notSettled = Symbol(`not-settled`)

Potentially, there could be multiple missing values for different domain meanings.

Let me know what you think of this use? Is it a good replacement for null? Should everybody always use an Option?

Note: Symbols are not always easy to use, watch my talk Symbols complicated it all.

23