DEV Community

Cover image for Understanding never in TypeScript
Deepankar Bhade
Deepankar Bhade

Posted on • Updated on • Originally published at dpnkr.in

Understanding never in TypeScript

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 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 end point 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}`;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
};
Enter fullscreen mode Exit fullscreen mode

Now looking at it's type declarations

declare const runInfinitely: () => never;
declare const throwError: () => never;
Enter fullscreen mode Exit fullscreen mode

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 which throws error

Impossible types

Have you ever seen a variable with the type of string & number both? Well let's see how TypeScript handles it's type.

const impossibleType = string & number;
Enter fullscreen mode Exit fullscreen mode

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 which 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);
  }
};
Enter fullscreen mode Exit fullscreen mode

Here liveLife is a function that has a switch case who's 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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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 error.

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

And now the error is gone!

Recap

  • TypeScript will use a never type to represent a type which 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 learnt something new, thanks for reading!

A quick favour: was anything I wrote incorrect or misspelled, or do you still have questions? Feel free to message me on twitter.

Top comments (1)

Collapse
 
totally_chase profile image
Phantz • Edited

In brief - In type theory, there's a clear distinction between a value that doesn't exist and a value that is of no importance. Usually, the former is known as Void and the latter is known as Unit.

Typescript is one of the few mainstream languages to have both concepts, Void in type theory, is never in typescript. And Unit in type theory, is (similar to) void in typescript, as well as other languages.

The nomenclature can be a bit confusing there. C decided to name the "value of no importance", void. And other languages followed suit. In reality void merely represents a value of no importance, rather than nothing at all. After all, if a function computes fully and returns to its parent - even if it has a return type of void, it did return. The value associated with said return was simply of no importance, therefore Unit. Meanwhile, saying a function returns Void (i.e never in typescript) is equivalent to saying it's impossible for the function to ever compute fully and return to the parent.