never
in TypeScriptIn this blog post, 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.
For showing computed and inferred types in source code, I use the npm package asserttt
– e.g.:
// Types of values
assertType<string>('abc');
assertType<number>(123);
// Equality of types
type Pair<T> = [T, T];
type _ = Assert<Equal<
Pair<string>, [string,string]
>>;
never
is a bottom type If 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 (more information).never
is a bottom type.never
is the empty set When 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 types We 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).never
: exhaustiveness checks at compile time Let’s use the following enum to demonstrate how we can do exhaustiveness checks (if we exhaustively handled all cases) via never
at compile time:
enum Color { Red, Green }
This is a pattern that 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 colorToString(color: Color): string {
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);
}
}
Therefore we can use UnexpectedValueError
to enforce that the type of color
is never
:
class UnexpectedValueError extends Error {
constructor(
// Type enables type checking
value: never,
// Avoid exception if `value` is:
// - object without prototype
// - symbol
message = `Unexpected value: ${{}.toString.call(value)}`
) {
super(message)
}
}
Now we get a compile-time warning if we forget a case:
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 properties Given that no other type is assignable to never
, we can use it to forbid properties.
The type EmptyObject
forbids 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
In contrast, the type {}
is assignable from all objects and not a type for empty objects:
const obj3: {} = { prop: 123 };
The type NoIndices
forbids number keys but allows the string key 'prop'
:
type NoIndices = Record<number, never> & { prop?: boolean };
//===== Objects =====
const obj1: NoIndices = {}; // OK
const obj2: NoIndices = { prop: true }; // OK
// @ts-expect-error: Type 'string' is not assignable to type 'never'.
const obj3: NoIndices = { 0: 'a' }; // OK
//===== Arrays =====
const arr1: NoIndices = []; // OK
// @ts-expect-error: Type 'string' is not assignable to type 'never'.
const arr2: NoIndices = ['a'];
never
never
also serves as a marker for functions that never return – e.g.:
function infiniteLoop(): never {
while (true) {}
}
function throwError(message: string): never {
throw new Error(message);
}
TypeScript’s type inference takes such functions into consideration. For example, the inferred return type of returnStringIfTrue()
is string
because we invoke throwError()
in line A.
function returnStringIfTrue(flag: boolean) {
if (flag) {
return 'abc';
}
throwError('Flag must be true'); // (A)
}
type _ = Assert<Equal<
ReturnType<typeof returnStringIfTrue>,
string
>>;
If we omit line A then the inferred return type is 'abc' | undefined
:
function returnStringIfTrue(flag: boolean) {
if (flag) {
return 'abc';
}
}
type _ = Assert<Equal<
ReturnType<typeof returnStringIfTrue>,
'abc' | undefined
>>;
never | T
In principle we could use the type never | T
for a function that, in some cases, throws an exception and does not return normally. However there are two reasons against doing that:
never | T
is the same as T
(as we have seen previously in this post).never
in @types/node
In Node.js, the following functions have the return type never
:
process.exit()
process.abort()
assert.fail()
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