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 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) {
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
?
Let’s first examine how the inferred value of color
changes as we add cases:
function colorToString(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);
}
}
Therefore we can use UnexpectedValueError
to enforce that the type of color
is never
:
class UnexpectedValueError extends Error {
constructor(
value: never,
// Avoid exceptions for 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:
function colorToString(color: Color) {
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);
}
}
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