Explain distributed conditional types.
Distributed conditional types in TypeScript are a powerful mechanism that allows conditional types to 'distribute' over the members of a union type when the type parameter being checked is a naked type parameter in the extends clause. This enables sophisticated type transformations and filtering operations on union types.
What are Conditional Types?
Before diving into distribution, let's briefly recap conditional types. A conditional type selects one of two possible types based on a condition that checks the relationship between two types. Their syntax is T extends U ? X : Y, meaning 'if type T is assignable to type U, then the type is X; otherwise, it's Y'.
The Concept of Distribution
Distribution occurs when the type parameter on the left side of the extends keyword in a conditional type is a 'naked' type parameter, and that type parameter is instantiated with a union type. When this happens, TypeScript applies the conditional type to each member of the union individually, and then reunites the results into a new union type.
Essentially, if you have a conditional type type MyType<T> = T extends U ? A : B; and you call MyType<X | Y | Z>, TypeScript doesn't evaluate (X | Y | Z) extends U ? A : B. Instead, it evaluates (X extends U ? A : B) | (Y extends U ? A : B) | (Z extends U ? A : B).
Syntax and Mechanism
type Distributed<T> = T extends U ? X<T> : Y<T>;
If T is a union type like A | B | C, the type Distributed<T> effectively becomes:
(A extends U ? X<A> : Y<A>) |
(B extends U ? X<B> : Y<B>) |
(C extends U ? X<C> : Y<C>)
Practical Examples
Distributed conditional types are crucial for creating utility types that filter, transform, or extract specific parts of a union type.
Example 1: Filtering Non-Nullable Types
A common use case is removing null and undefined from a type. TypeScript's built-in NonNullable<T> utility type uses this principle.
type NonNullableCustom<T> = T extends null | undefined ? never : T;
type MyUnion = string | number | null | undefined;
type Result = NonNullableCustom<MyUnion>;
// Result is inferred as: string | number
Explanation of NonNullableCustom<MyUnion>:
string extends null | undefined ? never : stringevaluates tostring.number extends null | undefined ? never : numberevaluates tonumber.null extends null | undefined ? never : nullevaluates tonever.undefined extends null | undefined ? never : undefinedevaluates tonever.
The results are then reunited: string | number | never | never, which simplifies to string | number (as never is the bottom type and disappears in a union).
Example 2: Extracting Specific Members
You can use this to extract only specific types from a union, like string members.
type ExtractStrings<T> = T extends string ? T : never;
type MixedUnion = string | number | boolean;
type OnlyStrings = ExtractStrings<MixedUnion>;
// OnlyStrings is inferred as: string
Explanation of ExtractStrings<MixedUnion>:
string extends string ? string : neverevaluates tostring.number extends string ? number : neverevaluates tonever.boolean extends string ? boolean : neverevaluates tonever.
The reunited union is string | never | never, which simplifies to string.
When Does Distribution Occur?
Distribution only occurs when the type parameter being checked (the 'left side' of extends) is a 'naked type parameter'. This means it's not wrapped in another type constructor. For example, T in T extends U is a naked type parameter, but [T] in [T] extends [U] or Promise<T> in Promise<T> extends Promise<U> are not.
type NotDistributed<T> = [T] extends [string] ? T : never;
type NakedUnion = string | number;
type ResultNotDistributed = NotDistributed<NakedUnion>;
// ResultNotDistributed is inferred as: never
// Explanation:
// [string | number] extends [string] ? (string | number) : never;
// The condition '[string | number] extends [string]' is false, because 'number' is not assignable to 'string'.
// Therefore, the result is 'never'. If it distributed, it would be 'string'.
Conclusion
Distributed conditional types are a fundamental concept in advanced TypeScript, enabling the creation of highly flexible and powerful utility types. They are essential for manipulating and refining union types, allowing developers to precisely control type inference and achieve complex type-level logic.