When conditional types act on a generic type, they become distributive when given a union type.
For example, take the following:
type ToArray<T> = T extends any ? T[] : never;
If we apply a union type to the generic type, TypeScript will apply the conditional type on each of the members of the union.
type StringOrNumber = string | number ;
type StringArrayOrNumberArray = ToArray<StringOrNumber>;
// ^ string[] | number[];
This is in contrast to the non-distributive behaviour if we had not used conditional types. The generic type would act on the union type as a whole.
type MyArray<T> = T[]
type StringOrNumberArray = MyArray<StringOrNumber>;
// ^ (string | number)[]
To prevent distribution, you can wrap the checked type in square brackets:
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type StringOrNumberArray = ToArrayNonDistributive<StringOrNumber>;
// ^ (string | number)[]
We might think that the following helper type can test for never:
// Wrong implementation
type IsNever<T> = T extends never ? true : false;
// Correct
type ShouldBeFalse = IsNever<string>;
// ^ ShouldBeFalse: false
// Wrong
type ShouldBeTrue = IsNever<never>;
// ^ ShouldBeTrue: never
This is interesting. For some reason, IsNever<never>
returns never
instead of true
.
This is due to the distributive behaviour of conditional types:
- TypeScript takes each type in the union
never
and applies the conditional type to it. - Since
never
is an empty union, there are no types to apply the conditional type to. - The result is
never
.
Side track: We can verify that never
is indeed an empty union, by unioning it with another type:
type AOrNever = 'a' | never;
// ^ AOrNever: 'a'
Distributive conditional types are particularly useful for:
- Transforming union types
- Filtering union types
- Implementing complex type manipulations
For example, you can use them to extract certain types from a union:
type ExtractString<T> = T extends string ? T : never;
type StringOnly = ExtractString<'A' | 'B' | 2 | false>;
// ^ StringOnly: 'A'