neverIn 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);
}
}
ifThe 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).
nevernever 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