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
typeToArray <Type > =Type extends any ?Type [] : never;
ts
typeToArray <Type > =Type extends any ?Type [] : never;
extends
is like a if clause
, if Type
is string
, it would be:
ts
typeA = string extends any ? string[] : never;
ts
typeA = 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
typeStrArrOrNumArr =ToArray <string | number>;
ts
typeStrArrOrNumArr =ToArray <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
typeStringNumber = string | number;typeB2 =StringNumber extends any ?StringNumber [] : never;
ts
typeStringNumber = string | number;typeB2 =StringNumber extends any ?StringNumber [] : never;
Distributivity only works on generics. Above conditional type is not distributive.
ts
typeToStringArray <Type > =Type extends string ?Type [] : never;typeB3 =ToStringArray <StringNumber >;
ts
typeToStringArray <Type > =Type extends string ?Type [] : never;typeB3 =ToStringArray <StringNumber >;
Above generic is distributive, so B3 is ToStringArray<string> | ToStringArray<number>
, ToStringArray<string> | never
so string[]
.
ts
typeMyExclude <T ,E > =T extendsE ? never :T ;typeC1 =MyExclude <string | number, number>;
ts
typeMyExclude <T ,E > =T extendsE ? never :T ;typeC1 =MyExclude <string | number, number>;
Now we can understand why Exclude<T>
could be written with extends
, because it is distributive.
ts
typeMyExcludeReversed <T ,E > =E extendsT ? never :E ;typeC2 =MyExcludeReversed <number, string | number>;ĀtypeMyExclude2 <T ,E > =T extendsE ? [T ,E ] : never;typeC3 =MyExclude2 <number | string | boolean, number | string | bigint>;
ts
typeMyExcludeReversed <T ,E > =E extendsT ? never :E ;typeC2 =MyExcludeReversed <number, string | number>;ĀtypeMyExclude2 <T ,E > =T extendsE ? [T ,E ] : never;typeC3 =MyExclude2 <number | string | boolean, number | string | 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
typeMyExclude3 <T ,E > =T extendsE ? (E extends any ? [T ,E ] : never) : never;typeC3 =MyExclude3 <number | string | boolean, number | string | bigint>;
ts
typeMyExclude3 <T ,E > =T extendsE ? (E extends any ? [T ,E ] : never) : never;typeC3 =MyExclude3 <number | string | boolean, number | string | bigint>;
Huh? It doesnās seem to be result I expect, letās change it a little bit
ts
typeMyExclude4 <T ,E > =T extends any?E extends any? [T ,E ]: [never]: never;typeC3 =MyExclude4 <number | string | boolean, number | string | bigint>;
ts
typeMyExclude4 <T ,E > =T extends any?E extends any? [T ,E ]: [never]: never;typeC3 =MyExclude4 <number | string | boolean, number | string | bigint>;
This is more we wanted. Ok, letās break down the previous snippet.
ts
typeC3 =MyExclude3 <number | string | boolean, number | string | bigint>;
ts
typeC3 =MyExclude3 <number | string | boolean, number | string | bigint>;
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.