Photo by Valentin Lacoste on Unsplash

What if we stop using null?

Oleg Isonen
4 min readNov 12, 2018

--

Currently, DOM and Browser APIs seem to be using null in some cases and undefined in some others. While they had their historical reasons, the result of this inconsistency is that we have to rely on type coercion more often than we should be, which brings an entire set of problems.

What I would like to discuss though is what we would gain if we stop using null and stick with undefined in user code.

1. Semantics — fewer things to think about

Is there really such a big difference in semantics? To me, both null and undefined mean pretty much the same — an absence of a value. Consider this code for example:

const map = new Map();
map.get('abc'); // undefined
localStorage.get('abc'); // null

In one case we get undefined in another case, we get a null, but what difference does it make for consumer code really?

2. Global 'undefined' is safe now

Previously in ECMA 1 to 5.1, there was no undefined global property in the spec, there was only an Undefined type and value. So in order to safely use undefined you needed to use a variable with undefined value in a local scope, otherwise, the consequences could have been unpredictable:

window.undefined = '¯\_(ツ)_/¯';
var a;
a === undefined; // false

Since ECMA 6.0 / 2015, global undefined is read-only, so it is safe to use.

3. Explicit intent

If you really need to check if an object property is defined, there is an in operator, which is for some reason rarely used, but actually exists exactly for this purpose, so that you can distinguish a missing property from a property with an undefined value.

const obj = {a: undefined};
'a' in obj; // true
'b' in obj; // false

Notice that in operator will look in the prototype chain if own property is not defined. If you want to check own property only, use hasOwnProperty method.

4. Operator “typeof” works intuitively

It’s not a secret that typeof null in JavaScript returns an “object” for the better or worse. Whatever reasoning was behind it in the early days of the language, from a consumer standpoint, it doesn’t make much sense, since null is an “empty” value and usually you want to distinguish it from other value types. In reality, to have proper type checks we end up checking both typeof and !== null .

In the case of undefined, typeof undefined returns “undefined”, which gives us a nice consistent and intuitive type when we check for emptiness.

5. What about JSON.stringify()?

When we serialize data, JSON.stringify()removes all properties which have undefined values, which is nice, because we should not need to send them over the network and other places.

JSON.stringify({b: undefined, c: null}); // {"c": null}

However, if you want to keep undefined properties as they are, it’s very easy to do:

JSON.stringify(
{b: undefined, c: null},
(key, value) => value === undefined ? 'undefined' : value
); // {b: "undefined", "c": null}

The same works with JSON.parse().

6. Default function parameters

Since ECMAScript 2015, default function parameters were added to the language. In case you don’t pass a parameter or pass an undefined — the default value will be used.

const f = (a = 1) => a;
f(); // 1
f(undefined); // 1
f(null); // null

At this point, we should notice that the language actually wants us to use undefined for empty values, not null.

7. Destructuring with default values

Default values in destructuring assignment syntax, also introduced in ES2015, work the same way.

const {a = 1} = {a: null};
a; // null
const {a = 1} = {};
a; // 1
const {a = 1} = {a: undefined};
a; // 1
const [a = 1] = [];
a; // 1
const [a = 1] = [undefined];
a; // 1
const {a: {b = 1}} = {a: {b: undefined}};
b; // 1

Default values now can be even more useful, since they are usable not only in function parameters but also arrays, objects and even nested objects and arrays.

8. Typed languages and annotations

Since the rise of typed annotations for JavaScript, we also need to make the type system aware of null and undefined types.

For example in Flow, there is a special notation for eventually existing values, which is called Maybe Types.

// @flow
const acceptsMaybeNumber = (value: ?number) => {
// ...
}

acceptsMaybeNumber(42); // Works!
acceptsMaybeNumber(); // Works!
acceptsMaybeNumber(undefined); // Works!
acceptsMaybeNumber(null); // Works!
acceptsMaybeNumber("42"); // Error!

You have now to write code that checks for both, value not being an undefined and value not being a null.

At the same time, there is an optional value syntax that works when undefined value or no value at all has been passed, so in your code, you only need to check for undefined.

// @flow
const acceptsMaybeNumber = (value?: number) => {
if (value !== undefined) {
return value * 2;
}
}

acceptsMaybeNumber(42); // Works!
acceptsMaybeNumber(); // Works!
acceptsMaybeNumber(undefined); // Works!
acceptsMaybeNumber(null); // Error!
acceptsMaybeNumber("42"); // Error!

It becomes quite complex if we want to allow a property that can have null or undefined as a value or not be defined at all.

type A = {p: ?number};
const a: A = {}; // Error!

So in order to support all 3 states, we need to describe the optional property with a maybe type:

type A = {p?: ?number};
const a: A = {}; // Works!

As you can see, your codebase would be much cleaner if you just stick with undefined, since some language features don’t work with null and mixing both leads to additional checks everywhere.

It almost feels like JavaScript language wants us to use undefined all the way down.

--

--

Oleg Isonen

Eng Core Automation @Tesla, ex @webflow, React, CSSinJS and co. Creator of http://cssinjs.org. Stealing ideas is great. Semantics is just meaning. Twitter: htt