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:
- The value can be
null
orundefined
. In the end, these are both valid JavaScript primitives and people can use them in many ways. - 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.
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.
Latest posts
Supermarket Meeting: The Secret to Cutting Down Useless Meetings
A Supermarket Meeting is a consolidated approach to recurring meetings, where diverse topics are discussed in one weekly session, similar to how you’d buy various items in one trip to the supermarket. Instead of creating multiple specialized meetings for different subjects, a supermarket meeting gathers all related discussions into a single, regular time slot. This maximizes productivity, ensures efficient use of time, and keeps meetings engaging and relevant.
3 Mistakes That Give Microservices a Bad Name
I’m sad to see that microservices are falling in popularity among architects and developers. Some say they are unnecessarily complex or overengineered. That one needs to learn so many new tools and technologies. That they introduce problems we had already solved. However, many ‘do microservices’ (unintentionally) wrong…