What if we stop using null?
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'); // undefinedlocalStorage.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; // nullconst {a = 1} = {};
a; // 1const {a = 1} = {a: undefined};
a; // 1const [a = 1] = [];
a; // 1const [a = 1] = [undefined];
a; // 1const {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.