Let’s discuss the never
type: when to use it, how it works, and why it’s a powerful tool in your TypeScript arsenal.
When you’re looking for information on how something works, especially in TypeScript, it’s always a good idea to start with the official documentation. The only issue is that TypeScript documentation can sometimes be scattered across different sections. Nevertheless, you can always use the search feature as shown in the example below:
What is never
?
The first mention in the documentation tells us that never
is a bottom type, while unknown
is a top type.

Top Type
The top type is a type that is a supertype of all other types. In other words, all other types are its subtypes. I like this explanation: each type in a type system can be represented by a certain set of types — finite or infinite.
In the diagram below, you can see a large blue bubble, representing the infinite set of types that fall under unknown
.
This bubble includes, for example, the infinite set of string
types, the infinite set of number
types, the finite set of boolean
types (true
and false
), the null
and undefined
types, and others.
Bottom Type
On the other hand, the bottom type is a type that is not a supertype for any other type. However, all other types are supertypes of never
.
In other words, never
is the empty set of types. If we wanted to represent never
in the diagram, it would be an empty bubble or even the absence of a bubble. never
is nothing. In some programming languages, such as Kotlin, Scala, and Ceylon, the bottom type is literally called Nothing
.
Returning to our previous statements and analogy with the bubble:
never
is not a supertype for any other type—meaning that no other type, no set of types, no bubble can fit intonever
. We can’t putsomething
intonothing
.
declare let neverVar: never;
let someString: string = 'Hello';
let someNumber: number = 42;
let someBoolean: boolean = true;
// Let's try assigning values of other types to a variable of type never
// These lines will trigger compilation errors:
neverVar = someString; // Error: Type 'string' is not assignable to type 'never'
neverVar = someNumber; // Error: Type 'number' is not assignable to type 'never'
neverVar = someBoolean; // Error: Type 'boolean' is not assignable to type 'never'
- All other types are supertypes of
never
— meaning that any other type, any set of types, any bubble can containnever
. Becausenever
is emptiness. Isn’t there room for emptiness in any bubble? :)
let stringVar: string;
let numberVar: number;
let booleanVar: boolean;
let objectVar: object;
// Assume we have a variable with type `never`:
declare let neverVar: never;
// Values of never can be assigned to variables of any other type:
stringVar = neverVar; // Valid
numberVar = neverVar; // Valid
booleanVar = neverVar; // Valid
objectVar = neverVar; // Valid
These statements are also present in the following fragment of the TypeScript documentation, which discusses the assignability table (for never
as well).
Practical Use of never
Functions That Never Return a Value
The first thing that comes to mind, and something you can find in the official documentation.
The never type represents values which are never observed. In a return type, this means that the function throws an exception or terminates execution of the program.
function fail(msg: string): never {
throw new Error(msg);
}
function infiniteLoop(): never {
while (true) {
// Infinite loop
}
}
Notice that in both functions, it is explicitly stated that they return never
. If we remove the explicit return type, TypeScript will implicitly infer the return type as void
:
function fail(msg: string) /*: void*/ {
throw new Error(msg);
}
function infiniteLoop() /*: void*/ {
while (true) {
// Infinite loop
}
}
Difference Between void
and never
According to the documentation
void represents the return value of functions which don’t return a value. It’s the inferred type any time a function doesn’t have any return statements, or doesn’t return any explicit value from those return statements:
// The inferred return type is void
function noop() {
return;
}
The difference between void
and never
is actually quite subtle.
void
does not guarantee that the function truly does not return any value. void
only guarantees that whatever the function returns will be ignored and cannot be used in the code.
type VoidFunc = () => void;
const f1: VoidFunc = () => {
return true;
};
const f2: VoidFunc = () => true;
const f3: VoidFunc = function () {
return true;
};
//Error: Type 'void' is not assignable to type 'boolean'.
const a: boolean = f1();
//Error: Type 'void' is not assignable to type 'boolean'.
const b: boolean = f2();
//Error: Type 'void' is not assignable to type 'boolean'.
const b: boolean = f3();
On the other hand, never
guarantees that the function truly does not return any value. This means that the function always throws an error, enters an infinite loop, or terminates the execution of the program.
type NeverReturnsFunc = () => never;
type VoidFunc = () => void;
const fail: NeverReturnsFunc = () => {
throw new Error();
};
const infiniteLoop: NeverReturnsFunc = () => {
while (true) {}
};
//Error: Type '() => void' is not assignable to type 'NeverReturnsFunc
const voidFunc: NeverReturnsFunc = () => {};
//Error: A function returning 'never' cannot have a reachable end point.
function voidFunc2(): never {}
There is also a special case to remember regarding void
— when you explicitly specify void
as the return type, the function must not return anything; otherwise, you’ll get an error as expected:
function f1(): void {
//Error: Type 'boolean' is not assignable to type 'void'.
return true;
}
const f2 = (): void => {
//Error: Type 'boolean' is not assignable to type 'void'.
return true;
};
type VoidFunc = () => void;
const f3: VoidFunc = () => {
//It works, but looks bad
return true;
};
never
in switch
and if
A rather interesting use of never
is to ensure that all possible cases in a switch
or if
statement are handled:
type Animal = 'cat' | 'dog' | 'bird';
function handleAnimal(animal: Animal): string {
switch (animal) {
case 'cat':
return 'This is a cat';
case 'dog':
return 'This is a dog';
case 'bird':
return 'This is a bird';
default:
// If another type is added to `Animal`, TypeScript will show an error.
const exhaustiveCheck: never = animal;
throw new Error(`Unknown animal: ${animal}`);
}
}
In this example, we use never
for exhaustiveCheck
to ensure that all possible values of the Animal
type are handled.
If we add a new type to Animal
(e.g., "fish"
), TypeScript will highlight an error, indicating that another case needs to be added to the switch
statement.
To take this idea further, we can create a helper function that takes an argument that should never exist and throws an error:
function assertUnreachable(value: never): never {
throw new Error(`Missed a case! ${value}`);
}
Then, the previous example can be rewritten as follows:
function handleAnimal(animal: Animal): string {
switch (animal) {
case 'cat':
return 'This is a cat';
case 'dog':
return 'This is a dog';
case 'bird':
return 'This is a bird';
default:
return assertUnreachable(animal);
}
}
Moreover, if you use linters, particularly typescript-eslint
, there is a rule called switch-exhaustiveness-check.
If certain checks can be automated, why not take advantage of it? :)
never
for Exclusive OR (XOR)
In TypeScript, “or” is inclusive, meaning that if you have a union of types A | B
, it is interpreted as A
, B
, or both.
This is well illustrated by the following example:
interface TextMessage {
text: string;
}
interface AttachmentMessage {
attachment: string;
}
type ChatMessage = TextMessage | AttachmentMessage;
const messageOne: ChatMessage = {text: 'Hello, world!'};
const messageTwo: ChatMessage = {attachment: 'https://example.com/image.jpg'};
const messageThree: ChatMessage = {text: 'Hello!', attachment: 'https://example.com/image.jpg'};
All three examples above are valid, since ChatMessage
can be either TextMessage
, AttachmentMessage
, or both.
But if we want ChatMessage
to be either TextMessage
or AttachmentMessage
, but not both at the same time, we can take advantage of the never
type:
interface TextMessage {
text: string;
attachment?: never;
}
interface AttachmentMessage {
text?: never;
attachment: string;
}
type ChatMessage = TextMessage | AttachmentMessage;
const messageOne: ChatMessage = {text: 'Hello, world!'}; // Valid
const messageTwo: ChatMessage = {attachment: 'https://example.com/image.jpg'}; // Valid
const messageThree: ChatMessage = {text: 'Hello!', attachment: 'https://example.com/image.jpg'}; // Error: Type '{ text: string; attachment: string; }' is not assignable to type 'ChatMessage'.
Or a more universal solution is a helper type XOR
:
type Without<T1, T2> = {[P in Exclude<keyof T1, keyof T2>]?: never};
type XOR<T1, T2> = T1 | T2 extends object ? (Without<T1, T2> & T2) | (Without<T2, T1> & T1) : T1 | T2;
However, it is worth mentioning that while this approach is completely correct, in TypeScript it is more common to use discriminated (tagged) unions.
A tag (discriminant) is an additional field that indicates the type of the object. This allows TypeScript to determine which type of object is being used in a given case.
interface TextMessage {
type: 'text';
text: string;
}
interface AttachmentMessage {
type: 'attachment';
attachment: string;
}
type ChatMessage = TextMessage | AttachmentMessage;
const messageOne: ChatMessage = {type: 'text', text: 'Hello, world!'};
In the example above, the tag is the type
field, which indicates the type of the object. If we accidentally specify both fields, TypeScript will highlight an error:
const messageThree: ChatMessage = {type: 'text', text: 'Hello!', attachment: 'https://example.com/image.jpg'}; // Error: Object literal may only specify known properties, and 'attachment' does not exist in type 'TextMessage'.
Moreover, with this approach, working with the type is much more convenient; compare the following:
Suppose we have a function that processes messages. If we use the tagged union approach, it looks like this:
function handleMessage(message: ChatMessage) {
switch (message.type) {
case 'text':
console.log(message.text);
break;
case 'attachment':
console.log(message.attachment);
break;
default:
assertUnreachable(message);
}
}
And if we use the XOR approach, it looks like this:
function handleMessage(message: XOR<TextMessage, AttachmentMessage>) {
if ('text' in message) {
console.log(message.text);
} else {
console.log(message.attachment);
}
}
In my opinion, the first version looks more readable and understandable. Especially, the difference will be noticeable when the code scales.
never
in Conditional Types
Where I have particularly often encountered never
is when writing utility types. By the way, there is a great repository - Type Challenges, where you can practice writing such types.
This utility type excludes null
and undefined
values from a type, returning never
for any type that contains them.
type NonNullable<T> = T extends null | undefined ? never : T;
// Usage examples:
type A = NonNullable<boolean>; // boolean
type B = NonNullable<number | null>; // number
type C = NonNullable<string | undefined>; // string
This is a copy of the built-in utility type Exclude
— it removes all subtypes that match the specified subtype from the type.
type Exclude<T, U> = T extends U ? never : T;
// Usage examples:
type Union = string | number | boolean;
type ExcludeString = Exclude<Union, string>; // number | boolean
type ExcludeNumber = Exclude<Union, number>; // string | boolean
This utility type extracts the value from a Promise
, using never
for all types that are not a Promise
.
type PromiseValue<T> = T extends Promise<infer R> ? R : never;
// Usage examples:
type Value1 = PromiseValue<Promise<string>>; // string
type Value2 = PromiseValue<Promise<number>>; // number
type Value3 = PromiseValue<number>; // never (not a Promise)
Distribution Over Unions (Distributive Conditional Types) - A Peculiarity of never
When working with conditional types, you might come across one of the pitfalls of never
— how it distributes over unions.
Let’s consider the following example. We have a conditional type:
type BeOrNotToBe<T> = T extends 'Something is rotten in the state of Denmark' ? 'Be' : 'Not to be';
If we pass the string "Something is rotten in the state of Denmark"
to this type, it will return "Be"
, otherwise — "Not to be"
.
type GuesWhat = BeOrNotToBe<'Gues what'>;
// ^? type GuesWhat = "Not to be"
If we pass a union to this type, TypeScript will distribute the type over each element of the union. The example below will return a union of "Not to be" | "Be"
:
type MaybeBoth = BeOrNotToBe<'Gues what' | 'Something is rotten in the state of Denmark'>;
// type MaybeBoth = BeOrNotToBe<"Gues what'"> | BeOrNotToBe<"Something is rotten in the state of Denmark">
// type MaybeBoth = "Not to be" | "Be"
But what happens if we pass never
to this type?
type AndNow = BeOrNotToBe<never>;
// type AndNow = never
We might expect AndNow
to be "Not to be"
, but it is not.
Why does this happen and how can we prevent it?
This happens because TypeScript treats the never
type as an empty union. And if there is nothing to distribute, you get nothing back. It just works this way.
To avoid implicit distribution, you can use []
:
type BeOrNotToBe<T> = [T] extends ['Something is rotten in the state of Denmark'] ? 'Be' : 'Not to be';
type AndNow = BeOrNotToBe<never>;
What do you think AndNow
will be? Your first thought might be that it will be "Not to be"
, but no. It will be "Be"
. Why?
Remember, at the beginning, we stated that all existing types are supertypes of never
, so when we check if never
is a subtype of "Something is rotten in the state of Denmark"
, it turns out it is — never
is a subtype of any type.
If you didn’t understand this the first time, don’t worry — this is quite a tricky concept that is might worth revisiting multiple times to understand how distribution over unions works and how TypeScript works with never
.
The end
Congratulations, you’ve reached the end of this article. We have covered what never
is and how to use it. I hope this article was helpful to you. If you have any questions or comments, feel free to reach out through any of the provided contacts. Thank you for your attention!