DEV Community

loading...
Cover image for Replace null with ES6 Symbols

Replace null with ES6 Symbols

Robin Pokorny
A zealous JavaScript developer
Originally published at robinpokorny.com ・4 min read

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 }
Enter fullscreen mode Exit fullscreen mode

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`)
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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]"
Enter fullscreen mode Exit fullscreen mode

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`)
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (7)

Collapse
0916dhkim profile image
Donghyeon Kim

One potential problem I can see from this approach is that js symbols convert to true. Which means it can confuse people when used like this:

const None = Symbol(`None`);
if (None) console.log('Oopsie');
Enter fullscreen mode Exit fullscreen mode
Collapse
robinpokorny profile image
Robin Pokorny Author

Yeah, I see that point. However, with so many falsy values in JS, I would not use the same for null or undefined either.

const value: string | null | undefined = 

if (value) { /* a non-empty string – intention or oversight? */}

if (value == null) { /* a string */ }
Enter fullscreen mode Exit fullscreen mode

Maybe NaN is a better choice? :D

Collapse
0916dhkim profile image
Donghyeon Kim

Actually, I was caught off guard by empty strings a few times before. You have a good point.

Collapse
lukeshiru profile image
LUKESHIRU

I generally tend to have linting rules against null and just use undefined. May I ask for your point of view about symbol vs. undefined? I mean I agree null kinda sucks, but I believe undefined is good enough (and I also work with TS, so even in the type side I don't get the usefulness of symbols in this scenario).

Collapse
robinpokorny profile image
Robin Pokorny Author

Hey! I'm not sure if undefined is better than null, in most cases there is not much difference. The. benefit of null or a Symbol is that it is more explicit. For me, undefined is the value the interpreter, null or Symbols are for users (this is not how it works, it's how I look at it).
Anyway, nullable for me means either undefined or null and the biggest issue with it remains: there is only one (well, two :D) and you cannot control how others use it.

Collapse
lukeshiru profile image
LUKESHIRU

From my point of view there is a bunch of reasons to prefer undefined over null:

  • For consistency sake, is better to just have one "nullable" value, not two.
  • undefined is the default value for variables without a value, arguments that aren't set, missing properties in an object, and so on.
  • If I do typeof it isn't bugged as null, it actually returns "undefined".
  • I still can use ?. and ??.
  • I honesty don't see the value in differentiate between a value that isn't defined that one that I intentionally left blank, when we have to deal with both the same way.
  • Responses are more lightweight:
{ foo: "foo", bar: null }
// vs
{ foo: "foo" }
Enter fullscreen mode Exit fullscreen mode

Again, don't take this the wrong way, I'm not saying "this is the way", I'm just listing the reasoning behind my preference, and that's why I commented in the first place, because I wanted to get your reasoning behind preferring a symbol over undefined mainly (null I get, because I also don't use it at all).

Cheers!

Collapse
captainyossarian profile image
yossarian

Interesting point of view