Skip to Content
Pipe0 | TypeScript's `never` type is a 0-member-union in distributive types

Distributive Generics and “never” in Typescript

To follow along you need a basic understanding of:

🧙🏻 TypeScript
🧠 Generics

Learning how to use and create effective types can bend your mind in unexpected ways, especially when you combine advanced types.

In this post, I’m exploring a problem I recently faced involving distributive generics and the never type.

Let’s start with a short intro about the never type and distributive conditional types.

Skip to the examples if you are familiar with both.

The never type

There are many ways to think about Typescript’s type system. One common way to think about types is as mathematical sets. While this sounds intimidating, it’s not too bad if you know a bit of TypeScript already.

It’s important to remember that types can be more or less specific.

If a type is not very specific, many values can be assigned to it.

The type with the lowest specificity is any. Any value can be assigned to a variable of type any.

let x: any = 9; // I'm a number x = "now I'm a string!" x = false // now I'm a boolean

If we think about types as sets, any is the set of all possible values. It is infinitely large but also the largest of all infinite sets (🤯 not all infinities are of the same size).

The never type is the exact opposite of the any type.

While any is the set of all values, never is so specific that there is no value that can be assigned to never. Never is the empty set. A set with no values.

💡
Tip

A type that refers to the set of all possible values is called the ‘top type’. In version 3.0, TypeScript introduced a new top type called unknown. unknown is a more type-safe counterpart of any. The top type unknown is the semantically correct opposite of never.

Distributive conditional types

When conditional types action on a generic type, they become distributive (see TypeScript docs)

This complicated sentence is best understood with an example. Let’s look at the following:

type ToArray<Type> = Type extends any ? Type[] : never; type StrArrOrNumArr = ToArray<string | number>; // StrArrOrNumArr = string[] | number[]

The StrArrOrNumArr type is string[] | number[]. But why not (string | number)[]?

Generics become distributive when used in conditional types. Here Type is the generic that is replaced with string | number.

ToArray maps over each member of the union. This behavior is unique to generics and conditional types.

Note

TypeScript will treat every type as a union. Simple types like string are treated as a union-of-one.

ToArray<string> | ToArray<number>; // think of it this way

The never types behaves weirdly

Let’s test our new knowledge of distributive conditional types. What do you expect Result1 and Result2 to be?

type Test<U> = U extends never ? string : number; // tip: only 'never' extends 'never' type Result1 = Test<string> // What is the type of Result1? type Result2 = Test<never>; // What's the type of Result2?

The answer may surprise you (it surprised me!).

  • Result1 is of type number.

This makes sense. never is an empty set so no type can extend it (except for never).

  • Result2 is of type never

Yes, the type of Result2 is never and the reason is distribution.

In the context of distributive types (and only there!) never is a 0-member-union. You simply cannot distribute/map over never (because it has 0 elements). The resulting type is always never!

This is a true mind-bender.

We can check that this is the case by disabling distribution. We can force typescript to not distribute and take our generic type parameter as a whole. We can do so by wrapping each side of the extends keyword in square brackets.

type Test<U> = [U] extends [never] ? string : number; type Result2 = Test<never>; // Result2 is `string`

Proving that never is a 0-member-union only in the context of distributive conditional types.

logo-dark
Add clay-like 🌈 data enrichment to your application. Fast.
Last updated on