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: