Understanding never in TypeScript

Understanding never in TypeScript

·

5 min read

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 (except never itself).
  • In a function expression or arrow function with no return type annotation, if the function has no return statements, or only return statements with expressions of type never, and if the endpoint of the function is not reachable (as determined by control flow analysis), the inferred return type for the function is never.
  • In a function with an explicit never return type annotation, all return statements (if any) must have expressions of type never 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;

impossible type

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.

Typescript being intelligent

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.

Mismatch in types

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.