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.
type ToArray<Type> = Type extends any ? Type[] : never;
extends
is like a if clause
, if Type
is string
, it would be:
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.
type StrArrOrNumArr = ToArray<string | number>;
// string[] | number[]
At first sight, above result might better be Array<string | number>
, but becaue Conditional Type is Distributive, it works more like below
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.
type StringNumber = string | number;
type B2 = StringNumber extends any ? StringNumber[] : never;
// StringNumber[]
Distributivity only works on generics. Above conditional type is not distributive.
type ToStringArray<Type> = Type extends string ? Type[] : never;
type B3 = ToStringArray<StringNumber>;
// string[]
Above generic is distributive, so B3 is ToStringArray<string> | ToStringArray<number>
, ToStringArray<string> | never
so string[]
.
type MyExclude<T, E> = T extends E ? never : T;
type C1 = MyExclude<string | number, number>;
// string
Now we can understand why Exclude<T>
could be written with extends
, because it is distributive.
type MyExcludeReversed<T, E> = E extends T ? never : E;
type C2 = MyExcludeReversed<number, string | number>;
// string
type MyExclude2<T, E> = T extends E ? [T, E] : never;
type C3 = MyExclude2<number | string | boolean, number | string | bigint>;
// [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
.
type MyExclude3<T, E> = T extends E ? (E extends any ? [T, E] : never) : never;
type C3 = MyExclude3<number | string | boolean, number | string | bigint>;
// [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
type MyExclude4<T, E> = T extends any
? E extends any
? [T, E]
: [never]
: never;
type C3 = MyExclude4<number | string | boolean, number | string | bigint>;
// [string, string] |
// [string, number] |
// [string, bigint] |
// [number, string] |
// [number, number] |
// [number, bigint]
This is more we wanted. Ok, let’s break down the previous snippet.
type C3 = MyExclude3<number | string | boolean, number | string | bigint>;
So for the first extends, T
is distributed, boolean
is filtered out, so it is identical to
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
[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.