1. Preface
TypeScript introduced two basic types, “never” and “unknown”, in version 2.0 and 3.0 respectively. The type system of TypeScript has been greatly improved.
However, when I take over the code, I find that many people are still stuck in the era of 1.0, the era of using any
everywhere. After all, JavaScript is a weakly typed dynamic language, and we used to not spend much time on type design. After the introduction of TypeScript, we even complained, “Why is this code getting more and more complicated?”.
In fact, we should think the other way around: an OOP programming paradigm is what code should look like after ES6.
2. top type, bottom type in TypeScript
In the type system design, there are two special types.
- Top type: known as the generic parent type, which is the type that can contain all values.
- Bottom type: represents a type that has no values, it is also called zero or empty type and is a subtype of all types.
In TypeScript 3.0, there are two top types (any and unknown) and one bottom type (never), as explained in the type system.
But some people think that any is also a bottom type, because any can also be a subtype of many types. But this argument is not strict, we can take a deeper look at unknown, any and never types.
3. unknown and any
3.1 unknown – for anything
When I read my colleagues’ code, I rarely see the unknown type appear. This does not mean that it is unimportant; on the contrary, it is a safe version of any type.
The difference between it and any is simple, see the following example.
|
|
Using any is like exploring a haunted house, the code is executed with ghosts everywhere. The combination of unknown with type guards, for example, ensures that code executes properly even when the upstream data structure is uncertain.
3.2 any
By using any, we’re giving up type checking, because it’s not used to describe specific types.
Before using it, we need to think of two things.
- whether it is possible to use a more specific type
- whether unknown can be used instead
If neither is possible, then any is the last option.
3.3 Reviewing previous type designs
Some existing type designs use any, which is not accurate enough. Here are two examples.
3.3.1 String()
String()
can take any argument and convert it to a string.
Combined with the unknown type introduced above, the arguments here can actually be designed as unknown, but the internal implementation will need more type guards. But the unknown type came later, so the initial design still uses any, which is what we see now.
3.3.2 JSON.parse()
I recently wrote a piece of code that involves deep copy.
|
|
Obviously, the output of JSON.parse() changes dynamically with the input (and may even throw an Error), and its function signature is designed as follows.
Can I use unknown here? Yes, but for the same reason as above, the function signature of JSON.parse()
was added to the TypeScript system before the unknown type appeared, otherwise its return type should be unknown.
4, never
mentioned above, the never
type is the empty type, that is, the value never exists type.
Value will never exist in two cases.
- if a function is executed when the exception is thrown, then the function will never exist return value (because the exception will be thrown to directly interrupt the program, which makes the program does not run to return the value of that step, that is, with the end of the unreachable, there will never be a return).
- the function executes an infinite loop of code (dead loop), so that the program can never run to the return value of the function, there is never a return.
4.1 The only bottom type
Since never is the only bottom type of typescript, it can represent a subtype of any type, so it can be assigned to any type.
We can use the set to understand never, unknown is the full set, never is the smallest unit (empty set), and any type contains never.
4.1.1 null/undefined and never
Here you may ask, null and undefined seem to be subtypes of any type, so why not bottom type. never is special in that no type is a subtype of it or can be assigned to it except itself. We can use the following example to compare.
|
|
The above example basically illustrates the difference between null/undefined and never, with never being the bottom one.
4.1.2 Why any is not a strict bottom type
When I read some articles, I found that people often say that any is both a top type and a bottom type, but this is not a strict statement.
We know from the above that no type can be assigned to never except never itself. does any satisfy this property? Obviously not, for a very simple example.
And why do we say never is the bottom type? Wikipedia explains it this way.
A function whose return type is bottom (presumably) cannot return any value, not even the zero size unit type. Therefore a function whose return type is the bottom type cannot return.
It is also easy to see from this that in a type system, bottom type is unique in that it uniquely describes the case of a function that returns nothing. So, with never, any heresy that is not type-checked is definitely not a bottom type.
4.2 The beauty of never
Never has the following usage scenarios.
- Unreachable code checking: mark unreachable code and get compilation hints.
- Type operation: as a minimal factor in type operations.
- Exhaustive Check: Create compilation hints for compound types.
- ……
There is no denying that never is a wonderful thing. From a set theory perspective, it is an empty set, so it can bring a lot of convenience to our type work through some properties of empty sets. Next, let’s talk about each usage scenario in detail.
4.2.1 Unreachable code check
A beginner wrote the following line of code.
Don’t laugh, it’s a real possibility. Of course at this point if you use ts, it will give you a compiler hint.
|
|
Because the return type of process.exit()
is defined as never, what comes after it is naturally the “unreachable code”.
Other possible scenarios are listening to sockets.
In general, we manually mark the return value of a function as never to help the compiler recognize “unreachable code” and to help us narrow (narrow) the type. Here is an example of unmarking.
Since the compiler does not know that throwError is a no-return function, the code after throwError()
is assumed to be reachable in any case, leaving the compiler with the misconception that the type of msg is string | undefined.
If the never type is marked, then the type of msg will be narrowed to string after the null check.
4.2.2 Type operations
4.2.2.1 Minimal factors
As mentioned above never can be understood as an empty set, then it will satisfy the following operation rules.
That is, never is the smallest factor of a type operation. These rules help us simplify some trivial type operations, for example, multiple Promise
s like Promise.race
merging, where sometimes it is impossible to know exactly the timing and return result. Now we use a Promise.race
to merge a Promise
that has a network request return value with another Promise
that will be rejected
within a given time.
The following is an implementation of a timeout function that throws an Error if the specified time is exceeded, and since it returns nothing, the result is defined as Promise<never>
.
Good, then the compiler will infer the return value of Promise.race
, because race takes the result of the first Promise
to complete, so in the example above, it has a function signature like this.
|
|
Substituting in fetchData and timeout, A would be { userName: string }
, while B would be never
. Therefore, the function outputs a promise
return value of type { userName: string } | never
. And since never
is the minimum factor, it can be eliminated. So the return value can be reduced to { userName: string }
, which is exactly what we want.
What happens if we use any
or unknown
here?
any is well understood, it passes normally, but it is equivalent to no type checking.
unknown is ambiguous and requires us to narrow the type manually.
When we strictly use never to describe “unreachable code”, the compiler can help us to narrow the type exactly, so that the code is the document.
4.2.2.2 Use in conditional types
We often see never in conditional types, which are used to indicate the else case.
For the above two conditional types for deriving function arguments and return values, we can get a hint from the compiler even if the T passed in is a non-function type.
Never also cleverly plays its role as a minimal factor when narrowing union types. Take, for example, the following example of excluding null
and undefined
from T
.
|
|
4.2.3 Exhaustive Check
Complex types such as union types and algebraic data types can be combined with the switch statement to narrow the types.
|
|
If someone later modifies the All
type, it will find that a compile error has been generated.
|
|
In the default branch, val is narrowed to Baz
, making it impossible to assign a value to never and generating a compile error. Developers can realize that handleValue needs to have Baz-specific processing logic in it. By doing this, you can ensure that handleValue always exhausts all possible types.
5. Conclusion
For students who value type specification and code design, TypeScript is not a shackle, but a pragmatic language. By learning more about the use and status of never and unknown in the TypeScript type system, you can learn a lot about type system design and set theory, and organize reliable and safe code by narrowing types in practice.