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.
tstypeToArray <Type > =Type extends any ?Type [] : never;
tstypeToArray <Type > =Type extends any ?Type [] : never;
extends is like a if clause, if Type is string, it would be:
tstypeA = string extends any ? string[] : never;
tstypeA = string extends any ? string[] : never;
The any here is nothing special, we can use unknown or {} or Object as well to make it truthy.
tstypeStrArrOrNumArr =ToArray <string | number>;
tstypeStrArrOrNumArr =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
tsToArray<A | B | C> = ToArray<A> | ToArray<B> | ToArray<C>;
tsToArray<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.
tstypeStringNumber = string | number;typeB2 =StringNumber extends any ?StringNumber [] : never;
tstypeStringNumber = string | number;typeB2 =StringNumber extends any ?StringNumber [] : never;
Distributivity only works on generics. Above conditional type is not distributive.
tstypeToStringArray <Type > =Type extends string ?Type [] : never;typeB3 =ToStringArray <StringNumber >;
tstypeToStringArray <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[].
tstypeMyExclude <T ,E > =T extendsE ? never :T ;typeC1 =MyExclude <string | number, number>;
tstypeMyExclude <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.
tstypeMyExcludeReversed <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>;
tstypeMyExcludeReversed <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.
tstypeMyExclude3 <T ,E > =T extendsE ? (E extends any ? [T ,E ] : never) : never;typeC3 =MyExclude3 <number | string | boolean, number | string | bigint>;
tstypeMyExclude3 <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
tstypeMyExclude4 <T ,E > =T extends any?E extends any? [T ,E ]: [never]: never;typeC3 =MyExclude4 <number | string | boolean, number | string | bigint>;
tstypeMyExclude4 <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.
tstypeC3 =MyExclude3 <number | string | boolean, number | string | bigint>;
tstypeC3 =MyExclude3 <number | string | boolean, number | string | bigint>;
So for the first extends, T is distributed, boolean is filtered out, so it is identical to
tsMyExclude3<number, number | string | bigint> |MyExclude3<string, number | string | bigint>;
tsMyExclude3<number, number | string | bigint> |MyExclude3<string, number | string | bigint>;
Then E is distributed, notice that T extends E still needs to be satisfied, so
tstype C3 =[number, number] |[never, string] |[never, bigint] |[never, number] |[string, string] |[never, bigint] |
tstype 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.
