Distributivity in Typescript

Official Doc

Yes, Conditional Types on Generics are Distributive.

When conditional types act on a generic type, they become distributive when given a union type.

Below is the official example.

ts
type ToArray<Type> = Type extends any ? Type[] : never;
ts
type ToArray<Type> = Type extends any ? Type[] : never;

extends is like a if clause, if Type is string, it would be:

ts
type A = string extends any ? string[] : never;
ts
type A = string extends any ? string[] : never;

The any here is nothing special, we can use unknown or {} or Object as well to make it truthy.

ts
type StrArrOrNumArr = ToArray<string | number>;
type StrArrOrNumArr = string[] | number[]
ts
type StrArrOrNumArr = ToArray<string | number>;
type StrArrOrNumArr = string[] | number[]

At first sight, above result might better be Array<string | number>, but becaue Conditional Type is Distributive, it works more like below

ts
ToArray<A | B | C> = ToArray<A> | ToArray<B> | ToArray<C>;
ts
ToArray<A | B | C> = ToArray<A> | ToArray<B> | ToArray<C>;

Above is what it means by Distributivity.

More examples

I’ve put a playground to show how it works.

ts
type StringNumber = string | number;
type B2 = StringNumber extends any ? StringNumber[] : never;
type B2 = StringNumber[]
ts
type StringNumber = string | number;
type B2 = StringNumber extends any ? StringNumber[] : never;
type B2 = StringNumber[]

Distributivity only works on generics. Above conditional type is not distributive.

ts
type ToStringArray<Type> = Type extends string ? Type[] : never;
type B3 = ToStringArray<StringNumber>;
type B3 = string[]
ts
type ToStringArray<Type> = Type extends string ? Type[] : never;
type B3 = ToStringArray<StringNumber>;
type B3 = string[]

Above generic is distributive, so B3 is ToStringArray<string> | ToStringArray<number>, ToStringArray<string> | never so string[].

ts
type MyExclude<T, E> = T extends E ? never : T;
type C1 = MyExclude<string | number, number>;
type C1 = string
ts
type MyExclude<T, E> = T extends E ? never : T;
type C1 = MyExclude<string | number, number>;
type C1 = string

Now we can understand why Exclude<T> could be written with extends, because it is distributive.

ts
type MyExcludeReversed<T, E> = E extends T ? never : E;
type C2 = MyExcludeReversed<number, string | number>;
type C2 = string
 
type MyExclude2<T, E> = T extends E ? [T, E] : never;
type C3 = MyExclude2<number | string | boolean, number | string | bigint>;
type C3 = [string, string | number | bigint] | [number, string | number | bigint]
ts
type MyExcludeReversed<T, E> = E extends T ? never : E;
type C2 = MyExcludeReversed<number, string | number>;
type C2 = string
 
type MyExclude2<T, E> = T extends E ? [T, E] : never;
type C3 = MyExclude2<number | string | boolean, number | string | bigint>;
type C3 = [string, string | number | bigint] | [number, string | number | bigint]

We can see only T is distributive, not E, it depends on the position in extends not in the generic.

What if we want to it to be distributive on E as well?, Simple, put E before extends.

ts
type MyExclude3<T, E> = T extends E ? (E extends any ? [T, E] : never) : never;
type C3 = MyExclude3<number | string | boolean, number | string | bigint>;
type C3 = [string, string] | [never, number] | [never, bigint] | [never, string] | [number, number]
ts
type MyExclude3<T, E> = T extends E ? (E extends any ? [T, E] : never) : never;
type C3 = MyExclude3<number | string | boolean, number | string | bigint>;
type C3 = [string, string] | [never, number] | [never, bigint] | [never, string] | [number, number]

Huh? It doesn’s seem to be result I expect, let’s change it a little bit

ts
type MyExclude4<T, E> = T extends any
? E extends any
? [T, E]
: [never]
: never;
type C3 = MyExclude4<number | string | boolean, number | string | bigint>;
type C3 = [string, string] | [string, number] | [string, bigint] | [number, string] | [number, number] | [number, bigint] | [false, string] | [false, number] | [false, bigint] | [...] | [...] | [...]
ts
type MyExclude4<T, E> = T extends any
? E extends any
? [T, E]
: [never]
: never;
type C3 = MyExclude4<number | string | boolean, number | string | bigint>;
type C3 = [string, string] | [string, number] | [string, bigint] | [number, string] | [number, number] | [number, bigint] | [false, string] | [false, number] | [false, bigint] | [...] | [...] | [...]

This is more we wanted. Ok, let’s break down the previous snippet.

ts
type C3 = MyExclude3<number | string | boolean, number | string | bigint>;
type C3 = [string, string] | [never, number] | [never, bigint] | [never, string] | [number, number]
ts
type C3 = MyExclude3<number | string | boolean, number | string | bigint>;
type C3 = [string, string] | [never, number] | [never, bigint] | [never, string] | [number, number]

So for the first extends, T is distributed, boolean is filtered out, so it is identical to

ts
MyExclude3<number, number | string | bigint> |
MyExclude3<string, number | string | bigint>;
ts
MyExclude3<number, number | string | bigint> |
MyExclude3<string, number | string | bigint>;

Then E is distributed, notice that T extends E still needs to be satisfied, so

ts
type C3 =
[number, number] |
[never, string] |
[never, bigint] |
[never, number] |
[string, string] |
[never, bigint] |
ts
type C3 =
[number, number] |
[never, string] |
[never, bigint] |
[never, number] |
[string, string] |
[never, bigint] |

Remove the duplicate one and we get the previous result.

Awesome, now we truely understand the distributivity.

😳 Would you like to share my post to more people ?    

❮ Prev: `{}` vs `object` vs `Object` in TypeScript.

Next: Build an accessible Popover component in React