A complete guide to TypeScript's never type
#typescriptSee discussions on Hacker News
TypeScript's never
type is very under-discussed, because it's not nearly as ubiquitous or inescapable as other types. A TypeScript beginner can probably ignore never
type as it only appears when dealing with advanced types, such as conditional types, or reading their cryptic type error messages.
The never
type does have quite a few good use cases in TypeScript. However, it also has its own pitfalls you need to be careful of.
In this blog post, I will cover:
- The meaning of
never
type and why we need it. - Practical applications and pitfalls of
never
. - a lot of puns 🤣
What is never type#
To fully understand never
type and its purposes, we must first understand what a type is, and what role it plays in a type system.
A type is a set of possible values. For example, string
type represents an infinite set of possible strings. So when we annotate a variable with type string
, such a variable can only have values from within that set, i.e. strings:
let foo: string = 'foo'
foo = 3 // ❌ number is not in the set of strings
In TypeScript, never
is an empty set of values. In fact, in Flow, another popular JavaScript type system, the equivalent type is called exactly empty
Since there’s no values in the set, never
type can never (pun-intended) have any value, including values of any
type. That’s why never
is also sometimes referred to as an uninhabitable type or a bottom type.
declare const any: any
const never: never = any // ❌ type 'any' is not assignable to type 'never'
The bottom type is how the TypeScript Handbook defines it. I found it makes more sense when we place never
in the type hierarchy tree, a mental model I use to understand subtyping
The next logical question is, why do we need never
type?
Why we need never type#
Just like we have zero in our number system to denote the quantity of nothing, we need a type to denote impossibility in our type system.
The word "impossibility" itself is vague. In TypeScript, “impossibility” manifests itself in various ways, namely:
- An empty type that can't have any value, which can be used to represent the following:
- Inadmissible parameters in generics and functions.
- Intersection of incompatible types.
- An empty union (a union type of nothingness).
- The return type of a function that never (pun-intended) returns control to the caller when it finishes executing, e.g.,
process.exit
in Node- Not to confuse it with
void
, asvoid
means a function doesn’t return anything useful to the caller.
- Not to confuse it with
- An else branch that should never (pun-intended... ok I think that's enough puns for today) be entered in a condition type
- The fulfilled value's type of a rejected
promise
const p = Promise.reject('foo') // const p: Promise<never>
How never works with unions and intersections#
Analogous to how number zero works in addition and multiplication, never
type has special properties when used in union types and intersection types:
-
never
gets dropped from union types, similiar to when zero added to a number gives the same number.- e.g.
type Res = never | string // string
- e.g.
-
never
overrides other types in intersection types, similiar to when zero multiplying a number gives zero.- e.g.
type Res = never & string // never
- e.g.
These two behaviors/characteristics of never
type lay the foundation for some of its most important use cases that we will see later on.
How to use never type#
While you probably wouldn’t find yourself use never
a lot, there are quite a few legit use cases for it:
Annotate inadmissible function parameters to impose restrictions#
Since we can never assign a value to never
type, we can use it to impose restrictions on functions for various use cases.
Ensure exhaustive matching within switch and if-else statement#
If a function can only take one argument of never
type, that function can never be called with any non-never
value (without the TypeScript compiler yelling at us):
function fn(input: never) {}
// it only accepts `never`
declare let myNever: never
fn(myNever) // ✅
// passing anything else (or nothing) causes a type error
fn() // ❌ An argument for 'input' was not provided.
fn(1) // ❌ Argument of type 'number' is not assignable to parameter of type 'never'.
fn('foo') // ❌ Argument of type 'string' is not assignable to parameter of type 'never'.
// cannot even pass `any`
declare let myAny: any
fn(myAny) // ❌ Argument of type 'any' is not assignable to parameter of type 'never'.
We can use such a function to ensure exhaustive matching within switch and if-else statement: by using it as the default case, we ensure that all cases are covered, since what remains must be of type never
. If we accidentally leave out a possible match, we get a type error. For example:
function unknownColor(x: never): never {
throw new Error("unknown color");
}
type Color = 'red' | 'green' | 'blue'
function getColorName(c: Color): string {
switch(c) {
case 'red':
return 'is red';
case 'green':
return 'is green';
default:
return unknownColor(c); // Argument of type 'string' is not assignable to parameter of type 'never'
}
}
Partially disallow structural typing#
Let’s say we have a function that accepts a parameter of either the type VariantA
or VariantB
. But, the user mustn’t pass a type encompassing all properties from both types, i.e., a subtype of both types.
We can leverage a union type VariantA | VariantB
for the parameter. However, since type compatibility in TypeScript is based on structural subtyping, passing an object type that has more properties than the parameter’s type has to a function is allowed (unless you pass object literals):
type VariantA = {
a: string,
}
type VariantB = {
b: number,
}
declare function fn(arg: VariantA | VariantB): void
const input = {a: 'foo', b: 123 }
fn(input) // TypeScript doens't complain but this shouldn't be allowed for our use case
The above code snippet doesn't give us a type error in TypeScript.
By using never
, we can partially disable structural typing and prevent users from passing object values that include both properties:
type VariantA = {
a: string
b?: never
}
type VariantB = {
b: number
a?: never
}
declare function fn(arg: VariantA | VariantB): void
const input = {a: 'foo', b: 123 }
fn(input) // ❌ Types of property 'a' are incompatible
Prevent unintended API usage#
Let’s say we want to create a Cache
instance to read and store data from/to it:
type Read = {}
type Write = {}
declare const toWrite: Write
declare class MyCache<T, R> {
put(val: T): boolean;
get(): R;
}
const cache = new MyCache<Write, Read>()
cache.put(toWrite) // ✅ allowed
Now, for some reason we want to have a read-only cache only allowing for reading data via the get
method. We can type the argument of the put
method as never
so it can’t accept any value passed in it:
declare class ReadOnlyCache<R> extends MyCache<never, R> {}
// Now type parameter `T` inside MyCache becomes `never`
const readonlyCache = new ReadOnlyCache<Read>()
readonlyCache.put(data) // ❌ Argument of type 'Data' is not assignable to parameter of type 'never'.
Unrelated to
never
type, as a side note, this might not be a good use case of derived classes. I am not really an expert on object-oriented programming, so please use your own judgment.
Denote theoretically unreachable conditional branches#
When using infer
to create an additional type variable inside a conditional type, we must add an else branch for every infer
keyword:
type A = 'foo';
type B = A extends infer C ? (
C extends 'foo' ? true : false// inside this expression, C represents A
) : never // this branch is unreachable but we cannot omit it
Why is this extends infer
combo useful?
In my previous post I mentioned how you can create declare “local (type) variable” together with extends infer
. Check it out here if you haven’t seen it.
Filter out union members from union types#
Beside denoting impossible branches, never
can be used to filter out unwanted types in conditional types.
As we have discussed this before, when used as a union member, never
type is removed automatically. In other words, the never
type is useless in a union type.
When we are writing a utility type to select union members from a union type based on certain criteria, never
type's uselessness in union types makes it the perfect type to be placed in else branches.
Let's say we want a utility type ExtractTypeByName
to extract the union members with the name
property being string literal foo
and filter out those that don't match:
type Foo = {
name: 'foo'
id: number
}
type Bar = {
name: 'bar'
id: number
}
type All = Foo | Bar
type ExtractTypeByName<T, G> = T extends {name: G} ? T : never
type ExtractedType = ExtractTypeByName<All, 'foo'> // the result type is Foo
See how this works in detail
Here are a list of steps TypeScript folllows to evaluate and get the resultant type:
- Conditional types are distributed over union types (namely,
Name
in this case):type ExtractedType = ExtractTypeByName<All, Name> ⬇️ type ExtractedType = ExtractTypeByName<Foo | Bar, 'foo'> ⬇️ type ExtractedType = ExtractTypeByName<Foo, 'foo'> | ExtractTypeByName<Bar, 'foo'>
- Substitue the implementation and evaluate separately
type ExtractedType = Foo extends {name: 'foo'} ? Foo : never | Bar extends {name: 'foo'} ? Bar : never ⬇️ type ExtractedType = Foo | never
- Remove
never
from the uniontype ExtractedType = Foo | never ⬇️ type ExtractedType = Foo
Filter out keys in mapped types#
In TypeScript, types are immutable. If we want to delete a property from an object type, we must create a new one by transforming and filtering the existing one. When we conditionally re-map keys in mapped types to never
, those keys get filtered out.
Here’s an example for a Filter
type that filters out object type properties based on their value types.
type Filter<Obj extends Object, ValueType> = {
[Key in keyof Obj
as ValueType extends Obj[Key] ? Key : never]
: Obj[Key]
}
interface Foo {
name: string;
id: number;
}
type Filtered = Filter<Foo, string>; // {name: string;}
Narrow types in control flow analysis#
When we type a function’s return value as never
, that means the function never returns control to the caller when it finishes executing. We can leverage that to help control flow analysis to narrow down types.
A function can never return for several reasons: it might throw an exception on all code paths, it might loop forever, or it exits from the program e.g.
process.exit
in Node.
In the following code snippet, we use a function that returns never
type to strip away undefined
from the union type for foo
:
function throwError(): never {
throw new Error();
}
let foo: string | undefined;
if (!foo) {
throwError();
}
foo; // string
Or invoke throwError
after ||
or ??
operator:
let foo: string | undefined;
const guaranteedFoo = foo ?? throwError(); // string
Denote impossible intersections of incompatible types#
This one might feel more like a behavior/characteristic of the TypeScript language than a practical application for never
. Nevertheless, it’s vital for understanding some of the cryptic error messages you might come across.
You can get never
type by intersecting incompatible types
type Res = number & string // never
And you get never
type by intersecting any types with never
type Res = number & never // never
It gets complicated for object types...
When intersecting object types, depending on whether or not the disjoint properties are considered as discriminant properties (basically literal types or unions of literal types), you might or might not get the whole type reduced to never
In this example only name
property becames never
since string
and number
are not discriminant properties
type Foo = {
name: string,
age: number
}
type Bar = {
name: number,
age: number
}
type Baz = Foo & Bar // {name: never, age: number}
In the following example, the whole type Baz
is reduced to never
because a boolean is a discriminant property (a union of true | false
)
type Foo = {
name: boolean,
age: number
}
type Bar = {
name: number,
age: number
}
type Baz = Foo & Bar // never
Check out this PR to learn more.
How to read never type (from error messages)#
You might have gotten error messages involving an unexpected never
type from code you didn’t annotate with never
explicitly. That’s usually because the TypeScript compiler intersects the types. It does this implicitly for you to retain type safety and to ensure soundness.
Here’s an example (play with it in TypeScript playground) that I used in my previous blog post on typing polymorphic functions:
type ReturnTypeByInputType = {
int: number
char: string
bool: boolean
}
function getRandom<T extends 'char' | 'int' | 'bool'>(
str: T
): ReturnTypeByInputType[T] {
if (str === 'int') {
// generate a random number
return Math.floor(Math.random() * 10) // ❌ Type 'number' is not assignable to type 'never'.
} else if (str === 'char') {
// generate a random char
return String.fromCharCode(
97 + Math.floor(Math.random() * 26) // ❌ Type 'string' is not assignable to type 'never'.
)
} else {
// generate a random boolean
return Boolean(Math.round(Math.random())) // ❌ Type 'boolean' is not assignable to type 'never'.
}
}
The function returns either a number, a string, or a boolean depending on the type of argument we pass. We use an indexes access ReturnTypeByInputType[T]
to retrieve the corresponding return type.
However, for every return statement we have a type error, namely: Type X is not assignable to type 'never'
where X
is string or number or boolean, depending on the branch.
This is where TypeScript tries to help us narrow down the possibility of problematic states in our program: each return value should be assignable to the type ReturnTypeByInputType[T]
(as we annotated in the example) where ReturnTypeByInputType[T]
at runtime could end up being either a number, a string, or a boolean.
Type safety can only be achieved if we make sure that the return type is assignable to all possible ReturnTypeByInputType[T]
, i.e. the intersection of number , string, and boolean.
And what’s the intersection of these 3 types? It’s exactly never
as they are incompatible with each other. That’s why we are seeing never
in the error messages.
To work around this, you must use type assertions (or function overloads):
return Math.floor(Math.random() * 10) as ReturnTypeByInputType[T]
return Math.floor(Math.random() * 10) as never
Maybe another more obvious example:
function f1(obj: { a: number, b: string }, key: 'a' | 'b') {
obj[key] = 1; // Type 'number' is not assignable to type 'never'.
obj[key] = 'x'; // Type 'string' is not assignable to type 'never'.
}
obj[key]
could end up being either a string or a number depending on the value of key
at runtime. Therefore, TypeScript added this constraint, i.e., any values we write to obj[key]
must be compatible with both types, string and number, just to be safe. So, it intersects both types and gives us never
type.
How to check for never#
Checking if a type is never
is harder than it should be.
Consider the following code snippet:
type IsNever<T> = T extends never ? true : false
type Res = IsNever<never> // never 🧐
Is Res
true
or false
? It might surprise you that the answer is neither: Res
is actually never
. In fact,
It definitely threw me off the first time I came across this. Ryan Cavanaugh explained this in this issue. It boils down to:
- TypeScript distributes union types in conditional types automatically
never
is an empty union- Therefore, when distribution happens there’s nothing to distribute over, so the conditional type resolves to
never
again.
The only workaround here is to opt out of the implicit distribution and to wrap the type parameter in a tuple:
type IsNever<T> = [T] extends [never] ? true : false;
type Res1 = IsNever<never> // 'true' ✅
type Res2 = IsNever<number> // 'false' ✅
This is actually straight out of TypeScript’s source code and it would be nice if TypeScript could expose this externally.
In summary#
We covered quite a lot in this blog post:
- First, we talked about
never
type's definition and purposes. - Then, we talked about its various use cases:
- imposing restrictions on functions by leveraging the fact that
never
is an empty type - filtering out unwanted union members and object type's properties
- aiding control flow analysis
- denoting invalid or unreachable conditional branches
- imposing restrictions on functions by leveraging the fact that
- We also talked about why
never
can come up unexpectedly in type error messages due to implicit type intersection - Finally, we covered how you can check if a type is indeed
never
type.
Special thanks to my friend Josh for reviewing this post and giving invaluable feedback!