never
In this chapter, we look at the special TypeScript type never
which, roughly, is the type of things that never happen. As we’ll see, it has a surprising number of applications.
never
is a bottom typeIf we interpret types as sets of values then:
Sub
is a subtype of type Sup
(Sub <: Sup
)
Sub
is a subset of Sup
(Sub ⊂ Sup
).
Two kinds of types are special:
T
includes all values and all types are subtypes of T
.
B
is the empty set and a subtype of all types.
In TypeScript:
any
and unknown
are top types and explained in “The top types any
and unknown
” (§14).
never
is a bottom type.
never
is the empty setWhen computing with types, type unions are sometimes used to represent sets of (type-level) values. Then the empty set is represented by never
:
type _ = [
Assert<Equal<
keyof {a: 1, b: 2},
'a' | 'b' // set of types
>>,
Assert<Equal<
keyof {},
never // empty set
>>,
];
Similarly, if we use the type operator &
to intersect two types that have no elements in common, we get the empty set:
type _ = Assert<Equal<
boolean & symbol,
never
>>;
If we use the type operator |
to compute the union of a type T
and never
then the result is T
:
type _ = Assert<Equal<
'a' | 'b' | never,
'a' | 'b'
>>;
never
: filtering union typesWe can use conditional types to filter union types:
type KeepStrings<T> = T extends string ? T : never;
type _ = [
Assert<Equal<
KeepStrings<'abc'>, // normal instantiation
'abc'
>>,
Assert<Equal<
KeepStrings<123>, // normal instantiation
never
>>,
Assert<Equal<
KeepStrings<'a' | 'b' | 0 | 1>, // distributed instantiation
'a' | 'b'
>>,
];
We use two phenomena to make this work:
never
types returned in the false branch of KeepStrings
disappear (see previous section).
More information: “Filtering union types by conditionally returning never
” (§34.3)
never
: exhaustiveness checks at compile timeWith type inference, TypeScript keeps track of what values a variable still can have – e.g.:
function f(x: boolean): void {
assertType<false | true>(x); // (A)
if (x === true) {
return;
}
assertType<false>(x); // (B)
if (x === false) {
return;
}
assertType<never>(x); // (C)
}
In line A, x
can still have the value false
and true
. After we return if x
has the value true
, it can still have the value false
(line B). After we return if x
has the value false
, there are no more values this variable can have, which is why it has the type never
(line C).
This behavior is especially useful for enums and unions used like enums because it enables exhaustiveness checks (checking if we have exhaustively handled all cases):
enum Color { Red, Green }
The following pattern works well for JavaScript because it checks at runtime if color
has an unexpected value:
function colorToString(color: Color): string {
switch (color) {
case Color.Red:
return 'RED';
case Color.Green:
return 'GREEN';
default:
throw new UnexpectedValueError(color);
}
}
How can we support this pattern at the type level so that we get a warning if we accidentally don’t consider all member of the enum Color
? (The return type string
also keeps us safe but with the technique we are about to see, we even get protection if there is no return time. Additionally, we are also protected from illegal values at runtime.)
Let’s first examine how the inferred value of color
changes as we add cases:
function exploreSwitch(color: Color) {
switch (color) {
default:
assertType<Color.Red | Color.Green>(color);
}
switch (color) {
case Color.Red:
break;
default:
assertType<Color.Green>(color);
}
switch (color) {
case Color.Red:
break;
case Color.Green:
break;
default:
assertType<never>(color);
}
}
Once again, the type records what values color
still can have.
The following implementation of the class UnexpectedValueError
requires that the type of its actual argument be never
:
class UnexpectedValueError extends Error {
constructor(
// Type enables type checking
value: never,
// Only solution that can stringify undefined, null, symbols, and
// objects without prototypes
message = `Unexpected value: ${{}.toString.call(value)}`
) {
super(message)
}
}
Now we get a compile-time warning if we forget a case because we have not eliminated all values that color
can have:
function colorToString(color: Color): string {
switch (color) {
case Color.Red:
return 'RED';
default:
assertType<Color.Green>(color);
// @ts-expect-error: Argument of type 'Color.Green' is not
// assignable to parameter of type 'never'.
throw new UnexpectedValueError(color);
}
}
if
The exhaustiveness check also works if we handle cases via if
:
function colorToString(color: Color): string {
assertType<Color.Red | Color.Green>(color);
if (color === Color.Red) {
return 'RED';
}
assertType<Color.Green>(color);
if (color === Color.Green) {
return 'GREEN';
}
assertType<never>(color);
throw new UnexpectedValueError(color);
}
never
: forbidding propertiesGiven that no other type is assignable to never
, we can use it to forbid properties – e.g. those with string keys:
type EmptyObject = Record<string, never>;
// @ts-expect-error: Type 'number' is not assignable to type 'never'.
const obj1: EmptyObject = { prop: 123 };
const obj2: EmptyObject = {}; // OK
For more information, see “Forbidding properties via never
” (§18.6).
never
never
also serves as a marker for functions that never return – e.g.:
function throwError(message: string): never {
throw new Error(message);
}
If we call such functions, TypeScript knows that execution ends and adjusts inferred types accordingly. For more information, see “Return type never
: functions that don’t return” (§27.4).
Section “Better Support for never
-Returning Functions” in “Announcing TypeScript 3.7” by Daniel Rosenwasser for Microsoft
Blog post “The never
type and error handling in TypeScript” by Stefan Baumgartner