Nullability in TypeScript
A lot of errors that are triggered in JavaScript applications surprisingly arise from unforeseen or incorrect usages of null
or undefined
types and values, which in turn trigger TypeError
s or ReferenceError
s. Uncaught errors crash applications and force developers to find the locations of bugs and fix them, which obviously costs time that otherwise could be spend more productively.
TypeScript, if used correctly, allows us to shield against these aforementioned problems. To understand how we can protect ourselves from nullability issues, we need to first understand the way JavaScript was designed in regards to handling null
and undefined
.
Undefined type
Even though JavaScript might be considered a dynamically-typed language (which in the end means that the type is associated with the value of a variable, not the variable itself), it does have types underneath the mask. The undefined
type (some people might prefer the notation beginning with a capital letter) allows for one value only:undefined
, which is also one of JavaScript’s immutable primitives and happens to be a global object. There are two major usages of this type that I would like to discuss.
Undefined as a lack of property
Let’s define a variable x
.
let x = {};
If we try to reference x.y
we will receive undefined
. In this context, y
does not exist (or is undefined ) as a property of variable x
. It does not mean that x.y
exists and hold undefined
, although it might be treated as such.
Undefined as a variable value
Let’s define variable x
again.
let x = undefined;
Variable x
holds now undefined
. It is also able to hold a different value in the future, if there’s a need for that.
Void operator
As a trivia, JavaScript has a void
operator which always returns undefined
. This is useful when minifying JS code as we can replace each undefined
with void 0
and still get the same result in the runtime.
Null type
The null
type is the type that allows for only one value, the null
value, which is also an immutable primitive. The usage is relatively simple:
let y = null;
Variable y
holds null
, it might however be assigned a different value in the future, if required by a developer.
Is null needed?
A careful reader might realise at this point that null
usages can be replaced with undefined
when we want to express that a variable has no meaningful value (which is fundamentally different from having no value at all, which is a challenge in JavaScript as the language seems not to have a formal bottom type). The aforementioned approach has been advised by certain people (e.g. TypeScript team here, there’s a TSLint rule here, and Douglas Crockford spoke about it here) , however one might stumble across certain problems when implementing it:
- third-party libraries might use both
null
andundefined
and assign separate meanings to them, as it suits the needs of the problems being solved by these libraries, - DOM uses
null
, - JSON format has no explicit notion of
undefined
but it does have a notion ofnull
(all properties withundefined
values are lost during serialisation which is not the case withnull
values), - SQL databases support the
NULL
value for its fields, which might become a nuisance for developers building ORMs.
This proves that even though we may discard null
, we can do it safely only in the parts of our codebase that is completely separated from the outside world.
TypeScript’s handling of nullability
TypeScript itself is a statically-typed language, however it allows for disabling the type control by using any
and unknown
types. The creators decided that the word null will always be used in connection to both null
and undefined
types and values, which is a reasonable choice as we should not use the term bottom types as these two types hold values (despite of the fact these values are not meaningful).
If strictNullCheck
configuration parameter (in tsconfig.json
) is set to false
, then null
and undefined
types are always a subtype of every other defined type, which results in the following statement being valid:
let x: string = null;
This mode is tremendously beneficial for projects that have been written in JavaScript and are being migrated to TypeScript, but it doesn’t protect us from nullability errors.
Settings strictNullCheck
to true
separates the null
and undefined
types and the rest of the types, which renders the following statements illegal:
let x: string = null;
let y: string = undefined;
Defining nullable fields
There are six ways that I know of that one can define a structure with a potentially nullable property in strict null checking mode.
type A = {
x: string | null;
};type B = {
x: string | undefined;
};type C = {
x?: string;
};type D = {
x?: string | undefined;
};type E = {
x?: string | null
};type F = {
x?: string | null | undefined;
};
Type A is trivial, as it allows for property x
to hold a value that is either a string
or a null
.
Types B, C and D are identical in the way their respective property’s value might be handled, however, there is one subtle difference. The ?
modifier ensures that the property might not be set as a key on the object level. This means that an instance of type B is never an empty object, whereas instances of types C and D might be empty. The only distinction between C and D is that type D explicitly states that property x
is allowed to hold undefined
, however, for most developers they will be interchangeable (for a reference, please check out this linting rule).
Type E and F extend the previous ideas with null
values.
Bottom type
As it have been mentioned before, a bottom type is a type that has no intrinsically assignable values, and as such null
and undefined
types are not formally bottom types, although colloquially they might be regarded as such by the JavaScript community. The void
type, as an smart alias of undefined
, is neither a bottom type. TypeScript defines only one bottom type (one is enough): the never
type, which is often use when dealing with impossible-to-happen scenarios (e.g. a function that always throws an exception never returns any value, hence it’s return type is never
).
NonNullable
TypeScript defines a special type as a part of its standard library:
type NonNullable<T> = T extends null | undefined ? never : T;
The way we can use it in our codebases may not be exactly clear at the beginning, but I am goint to present you with one interesting usage of it.
Let’s declare one helper type and an array:
type NullableNumber = number | null | undefined;const nullableNumbers: ReadonlyArray<NullableNumber> = [1, 2, 3, null, undefined];
nullableNumbers
are a collection of numbers, null
and undefined
. We should be able to get rid of nullable values right away with a simple filter
function, right?
const numbers: ReadonlyArray<number> = nullableNumbers
.filter(n => n !== null && n !== undefined);
Wrong! It turns out the TypeScript compiler does not understand the subtle type narrowing here and throws a compile-time error, even though this is perfectly sound in runtime (even though it would work, the compiler must comprehend why). We can help the compiler understand what we did here by leveraging the NonNullable
type with a type guard:
const isNonNullable = <T>(t: T): t is NonNullable<T> => t !== null && t !== undefined;const nonNullableNumbers: ReadonlyArray<number> = nullableNumbers.filter(isNonNullable);
Now, the code is compilable and allows us to filter out nullable values.
Summary
I hope you enjoyed reading the article. Please note that this is but a brief introduction to a rather complex problem and just for TypeScript. The inventor of the null
reference, Tony Hoare, considers it a mistake. Nowadays the concept of nullability is still used by various mainstream languages, including C, C++, Java or Python, however the creators of some languages have made continuous efforts to obstruct access to bare pointers and empty references using quite elaborate techniques (for instance a reference in C++ is always guaranteed to hold a meaningful value).