I never really understood never
in my life because to understand never
you have to use the word never again and again.
So understanding never
can be quite baffling. If you're like me and ever had faced a similar issue then this blog should be able to explain it with the help of some examples.
Before taking you through the examples this is how TypeScript explains characteristics of never
in their release notes.
never
is a subtype of and assignable to every type.- No type is a subtype of or assignable to
never
(exceptnever
itself). - In a function expression or arrow function with no return type annotation, if the function has no
return
statements, or onlyreturn
statements with expressions of typenever
, and if the endpoint of the function is not reachable (as determined by control flow analysis), the inferred return type for the function isnever
. - In a function with an explicit
never
return type annotation, allreturn
statements (if any) must have expressions of typenever
and the end point of the function must not be reachable.
Let me break it down for you.
Basics
Let's start with a simple example
const logName = (s: string) => {
console.log(`Your name: ${s}`);
};
const returnName = (s: string): string => {
return `Your name: ${s}`;
};
Now if you look at the type declaration of these functions it's easy to understand logName
returns void
and returnName
returns string
type.
declare const logName: (s: string) => void;
declare const returnName: (s: string) => string;
Now if we log the logName
function we get undefined
.
This happens because a function that doesn't explicitly return a value, implicitly returns the value undefined in JavaScript.
const logName = (s: string) => {
console.log(`Your name: ${s}`);
};
console.log(logName('Deepankar'));
// Your name: Deepankar
// undefined
I added this example to explain that even if void
seems to not return any value but still returns undefined
this is inferred as void
in TypeScript.
Functions that never return
So what does happen if a function literally doesn't return anything? Well let's look at some examples.
const runInfinitely = () => {
while (true) {
console.log('Running');
}
};
const throwError = () => {
throw new Error('Bruh');
};
Now looking at its type declarations
declare const runInfinitely: () => never;
declare const throwError: () => never;
Awesome so we finally see the never
type now let's understand why
runInfinitely()
runs in an infinite loop and never breaks/returns anything and throwError()
runs and throws an exception which stops the program to run and never returns.
From these examples, we can conclude that Typescript infers the return type as never
if a function expression
- never breaks/returns anything
- has a throw statement that throws an error
Impossible types
Have you ever seen a variable with the type of string
& number
both? Well, let's see how TypeScript handles its type.
const impossibleType = string & number;
Now if we hover over the variable in vscode we should be able to see that impossibleType
has never
type.
Hence TypeScript will use a never
type to represent a type that is impossible to exist.
Exhaustive Checks
From the TypeScript handbook
When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never
type to represent a state which shouldn’t exist.
The never
type is assignable to every type; however, no type is assignable to never (except never itself). This means you can use narrowing and rely on never turning up to do exhaustive checking in a switch statement.
To understand this take the following example
const notPartOfLife = (n: never) => {};
type Life = 'Eat' | 'Sleep' | 'Code';
const liveLife = (life: Life) => {
switch (life) {
case 'Eat':
return 'Eating';
case 'Sleep':
return 'Eating';
case 'Code':
return 'Coding';
default:
return notPartOfLife(life);
}
};
Here liveLife
is a function that has a switch case whose default case would never run because it exhausts all the cases of Life
type.
TypeScript is intelligent enough to infer the type as never
if the conditional block is impossible to happen and that's why life
is inferred as never
.
Now let's add another value to the Life
type
const notPartOfLife = (n: never) => {};
type Life = 'Eat' | 'Sleep' | 'Code' | 'Play';
const liveLife = (life: Life) => {
switch (life) {
case 'Eat':
return 'Eating';
case 'Sleep':
return 'Eating';
case 'Code':
return 'Coding';
default:
return notPartOfLife(life);
}
};
Upon doing this we should be able to see this beautiful Typescript error. But don't worry it's helping us out here. Typescript was able to infer that type of life
would be Play
which is a string but notPartOfLife
function needs a param of type never
.This mismatch of types causes TypeScript to throw errors.
Fixing this is easy we will just add the case for Playing
.
const notPartOfLife = (n: never) => {};
type Life = 'Eat' | 'Sleep' | 'Code' | 'Play';
const liveLife = (life: Life) => {
switch (life) {
case 'Eat':
return 'Eating';
case 'Sleep':
return 'Eating';
case 'Code':
return 'Coding';
case 'Play':
return 'Playing';
default:
return notPartOfLife(life);
}
};
And now the error is gone!
Recap
- TypeScript will use a
never
type to represent a type that is impossible to exist. - The
never
type is assignable to every type; however, no type is assignable to never (except never itself). - TypeScript will infer
never
as return type if the function never returns / throws error.
Hope you’ve learned something new, thanks for reading!
A quick favor: was anything I wrote incorrectly or misspelled, or do you still have questions? Feel free to message me on twitter.