The never
and unknown
primitive types were introduced in Typescript v2.0
and v3.0 respectively.
These two types represent fundamental and complementary aspects of type theory.
Typescript is carefully designed according to principles of type theory;
but it is also a practical language,
and its features all have practical uses -
including never
and unknown
.
To understand those uses we will have to begin with the question,
what exactly are types?
When you get down to a fundamental definition a type is a set of possible
values,
and nothing more.
For example the type string
in Typescript is the set of all possible strings;
the type Date
is the set of all instances of the Date
class
(plus all structurally-compatible objects);
and the type Iterable<T>
is the set of all objects that implement the
iterable interface for the given type of iterated values.
Typescript is especially faithful to the Set-Theoretic basis for types;
among other features Typescript has union and intersection types.
A type like string | number
is called a "union" type because it literally is
the union of the set of all strings, and the set of all numbers.
Because string | number
contains all string
and all number
values it is
said to be a supertype of string
and of number
.
unknown
is the set of all possible values.
Any value can be assigned to a variable of type unknown
.
This means that unknown
is a supertype of every other type.
unkown
is called the top type for that reason.
never
is the empty set.
There is no value that can be assigned to variable of type never
.
In fact it is an error for the type of a value to resolve to never
because
that would be a contradiction.
The empty set can fit inside any other set,
so never
is a subtype of every other type.
That is why never
is called the bottom type.1
The bottom and top types have the useful properties of being the
identity element with respect to the union and intersection operations
respectively.
For any type T
,
https://gist.github.com/13c1cbf11f11bb8bec4f706b0adefe2a
This is analogous to the idea that adding zero to a number does not change it, and the same goes for multiplying a number by one. Zero is the identity element for addition, and one is the identity element for multiplication.
A union with the empty set does not add anything,
so never
is the identity with respect to unions.
An intersection selects the common elements between two sets,
but unknown
contains everything so unknown
is the identity with respect to
intersections.
never
is the only type that will "factor out" in a type union which makes it
indispensable for certain cases,
as we will see in the next section.
Let's write some code that makes a network request,
but that fails if the request takes too long.
We can do that by using Promise.race
to combine a promise for the network
response with a promise that rejects after a given length of time.
Here is a function to construct that second promise:
https://gist.github.com/6333c03375fd4492c9bf08d720ff902c
Note the return type:
because timeout
never calls resolve
we could use any type for the
promise type parameter,
and there would be no contradiction.
But the most specific type that will work is never
.
(By "most specific" I mean the type that represents the smallest set of
possible values.)
Now let's see timeout
in action.
https://gist.github.com/c49913ae5704777d14c677cc8bec1ceb
This works nicely.
But how does the compiler infer the correct the return type from that
Promise.race
call?
race
returns a single promise with the result or failure from the first
promise to settle.
For purposes of this example the signature of Promise.race
works like this:
https://gist.github.com/3a0a662a89a4220b91c4e88fdaca030d
The type of the resolved value in the output promise is a union of the
resolution types of the inputs.
The example above combines fetchStock
with timeout
so the input promise
resolution types are { price: number }
and never
,
and the resolution type of the output (the type of the variable stock
) should
be { price: number } | never
.
Because never
is the identity with respect to unions that type simplifies to
{ price: number }
,
which is what we want.
If we had used any type other than never
as the parameter of the return type
in timeout
things would not have worked out so cleanly.
If we had used any
we would have lost benefits of type-checking because
{ price: number } | any
is equivalent to any
.
If we had used unknown
then the type of stock
would be
{ price: number } | unknown
,
which does not simplify.
In that case we would not be able to access the price
property without
further type narrowing because the price
property would only be listed in one
branch of the union.
You will often see never
used in conditional types to prune unwanted
cases.
For example these conditional types extract the argument and return types from
a function type.
https://gist.github.com/dd83d9afbebc74c10f782c4629319c13
If T
is a function type then the compiler infers its argument types or return
type.
But if T
is not a function type then there is no sensible result for
Arguments<T>
or Return<T>
.
We use never
in the else branch of each condition to make that case an error.
https://gist.github.com/ad7b329bee13631725077b9b8307d2e6
Conditional pruning is also useful for narrowing union types.
Typescript's libraries include the NonNullable<T>
type
(source) which removes null
and undefined
from a union type.
The definition looks like this:
https://gist.github.com/2f6a7f52df4f9b8c209e04b0de8e1f63
This works because conditional types distribute over type unions.
Given any type of the form T extends U ? X : Y
when a union type is substituted for T
the type expands to distribute the
condition to each branch of that union type:
https://gist.github.com/3104c12705c1e9b283de33e0acebbec1
In each union branch every occurrence of T
is replaced by one
constituent from the substituted union type.
This also applies if T
appears in the true case instead of the false case,
or occurs inside of a larger type expression:
https://gist.github.com/a220ae03b09988680bdfa6ca2d123c72
So a type like NonNullable<string | null>
resolves according to these steps:
https://gist.github.com/7dab80dfe032baff9483d9da14c46077
The result is that given a union type NonNullable<T>
produces a potentially
narrowed type using never
to prune unwanted union branches.
Any value can be assigned to a variable of type unknown
.
So use unknown
when a value might have any type,
or when it is not convenient to use a more specific type.
For example a pretty-printing function should be able to accept any type of
value:
https://gist.github.com/16486e17dbcd3ce86955433c4f858555
You cannot do much with an unknown
value directly.
But you can use type guards to narrow the type,
and get accurate type-checking for blocks of code operating on narrowed types.
Prior to Typescript 3.0 the best way to write prettyPrint
would have been to
use any
for the type of x
.
Type narrowing works with any
the same way that it does with unknown
;
so the compiler can check that we used map
and join
correctly in the case
where x
is narrowed to an array type regardless of whether we use any
or
unknown
.
But using unknown
will save us if we make a mistake where we think that the
type has been narrowed,
but actually it has not:
https://gist.github.com/8c979bb764a6a2d3cdeba8be34fcfe6f
The isarray package does not include type definitions to turn the isArray
function into a type guard.
But we might use isarray without realizing that detail.
Because isArray
is not a type guard, and we used any
for the type of x
,
the type of x
remains any
in the if
body.
As a result the compiler does not catch the typo in this version of
prettyPrint
.
If the type of x
were unknown
we would have gotten this error instead:
Object is of type 'unknown'.
In addition using any
lets you cheat by performing operations that are not
necessarily safe.
unknown
keeps you honest.
The type of x
in prettyPrint
and the promise type parameter in the return
type of timeout
are both cases where a value could have any type.
The difference is that in timeout
the promise resolution value could
trivially have any type because it will never exist.
- Use
never
in positions where there will not or should not be a value. - Use
unknown
where there will be a value, but it might have any type. - Avoid using
any
unless you really need an unsafe escape hatch.
In general use the most specific type that will work.
never
is the most specific type because there is no set smaller than the
empty set.
unknown
is the least specific type because it contains all possible values.
any
is not a set, and it undermines type-checking;
so try to pretend that it does not exist when you can.
Footnotes
-
There is a crucial distinction between
never
andnull
: the typenull
is actually a singleton set meaning that it contains exactly one value, the valuenull
. Some languages treatnull
as though it is a subtype of every other type, in which case it is effectively a bottom type. (This includes Typescript if it is not configured with strict checking options.) But that leads to contradictions because, for example,null
is not actually present in the set of all strings. So please use the--strictNullChecks
compiler option to get contradiction-free treatment ofnull
! ↩