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 null
types.
When you first turn on strictNullChecks
, it may seem as though you have to add scores of if
statements checking for null
and 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[0, 2]
. - If the
nums
array is empty, the function will return[undefined, undefined]
. This sort of object with severalundefined
s 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 thatmin
andmax
will either both beundefined
or neither, but that information isn't represented in the type system.
Turning on 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 extent
:
|
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-null
:
|
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:
SubscribeEffective TypeScript shows you not just how to use TypeScript but how to use it well. Now in its second edition, the book's 83 items help you build mental models of how TypeScript and its ecosystem work, make you aware of pitfalls and traps to avoid, and guide you toward using TypeScript’s many capabilities in the most effective ways possible. Regardless of your level of TypeScript experience, you can learn something from this book.
After reading Effective TypeScript, your relationship with the type system will be the most productive it's ever been! Learn more »