A complete guide to TypeScript’s never type

#typescript

See discussions on Hacker News

This post has been translated into Korean

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, as void means a function doesn’t return anything useful to the caller.
  • 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
  • never overrides other types in intersection types, similiar to when zero multiplying a number gives zero.

    • e.g. type Res = never & string // never

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:

  1. 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'>
    
  2. Substitue the implementation and evaluate separately
    type ExtractedType = Foo extends {name: 'foo'} ? Foo : never 
                        | Bar extends {name: 'foo'} ? Bar : never
    ⬇️
    type ExtractedType = Foo | never
    
  3. Remove never from the union
    type 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
  • 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!