Chapter 4 of Effective TypeScript covers type design: the process of crafting your types to accurately model your domain. Design your types well and you'll have a happy, productive relationship with the type checker. Design them poorly and you'll find yourself constantly fighting unproductive battles with it. This item discusses a frequent source of problems in type design: where to put your
When you first turn on
strictNullChecks, it may seem as though you have to add scores of
if statements checking for
undefined values throughout your code. This is often because the relationships between null and non-null values are implicit: when variable A is non-null, you know that variable B is also non-null and vice versa. These implicit relationships are confusing both for human readers of your code and for the type checker.
Values are easier to work with when they're either completely null or completely non-null, rather than a mix. You can model this by pushing the null values out to the perimeter of your structures.
Suppose you want to calculate the min and max of a list of numbers. We'll call this the "extent." Here's an attempt:
The code type checks (without
strictNullChecks) and has an inferred return type of
number, which seems fine. But it has a bug and a design flaw:
- If the min or max is zero, it may get overridden. For example,
extent([0, 1, 2])will return
[1, 2]rather than
- If the
numsarray is empty, the function will return
[undefined, undefined]. This sort of object with several
undefineds will be difficult for clients to work with and is exactly the sort of type that this item discourages. We know from reading the source code that
maxwill either both be
undefinedor neither, but that information isn't represented in the type system.
strictNullChecks makes both of these issues more apparent:
The return type of
extent is now inferred as
(number | undefined), which makes the design flaw more apparent. This is likely to manifest as a type error wherever you call
The error in the implementation of
extent comes about because you've excluded
undefined as a value for
min but not
max. The two are initialized together, but this information isn't present in the type system. You could make it go away by adding a check for
max, too, but this would be doubling down on the bug.
A better solution is to put the min and max in the same object and make this object either fully
null or fully non-
The return type is now
[number, number] | null, which is easier for clients to work with. The min and max can be retrieved with either a non-null assertion:
or a single check:
By using a single object to track the extent, we've improved our design, helped TypeScript understand the relationship between null values, and fixed the bug: the
if (!result) check is now problem free.
A mix of null and non-null values can also lead to problems in classes. For instance, suppose you have a class that represents both a user and their posts on a forum:
While the two network requests are loading, the
posts properties will be
null. At any time, they might both be
null, one might be
null, or they might both be non-
null. There are four possibilities. This complexity will seep into every method on the class. This design is almost certain to lead to confusion, a proliferation of
null checks, and bugs.
A better design would wait until all the data used by the class is available:
UserPosts class is fully non-
null, and it's easy to write correct methods on it. Of course, if you need to perform operations while data is partially loaded, then you'll need to deal with the multiplicity of
null and non-
(Don't be tempted to replace nullable properties with Promises. This tends to lead to even more confusing code and forces all your methods to be async. Promises clarify the code that loads data but tend to have the opposite effect on the class that uses that data.)
- Avoid designs in which one value being
nullis implicitly related to another value being
nullvalues to the perimeter of your API by making larger objects either
nullor fully non-
null. This will make code clearer both for human readers and for the type checker.
- Consider creating a fully non-
nullclass and constructing it when all values are available.
strictNullChecksmay flag many issues in your code, it's indispensable for surfacing the behavior of functions with respect to null values.