Conditional types and distributive conditional types
TypeScript's conditional types and distributive conditional types are advanced features in the type system that enable dynamic derivation of more complex type relationships based on input types. Through conditional branching at the type level and automatic distribution mechanisms, they significantly enhance the flexibility of type programming.
Basics of Conditional Types
The syntax of conditional types resembles a ternary expression, formatted as T extends U ? X : Y
. When type T
is assignable to type U
, it returns type X
; otherwise, it returns type Y
. This mechanism allows conditional logic at the type level:
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
Conditional types are often combined with generics to create dynamic type inference. For example, implementing a utility type to extract the element type of an array:
type ElementType<T> = T extends (infer U)[] ? U : never;
type Numbers = ElementType<number[]>; // number
type Mixed = ElementType<(string | boolean)[]>; // string | boolean
Distributive Conditional Types
When conditional types operate on union types, distributive behavior occurs. This is one of the most powerful features in TypeScript's type system. Specifically, for T extends U ? X : Y
, if T
is a union type A | B | C
, the result is distributed as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
.
type ToArray<T> = T extends any ? T[] : never;
type Numbers = ToArray<number>; // number[]
type UnionArray = ToArray<number | string>; // number[] | string[]
The distributive feature is particularly useful for filtering union types. For example, implementing a utility to exclude specific types from a union:
type Exclude<T, U> = T extends U ? never : T;
type WithoutNumbers = Exclude<string | number | boolean, number>; // string | boolean
Type Inference in Conditional Types
The infer
keyword allows declaring temporary type variables in conditional types to capture deep type information. This is useful for deconstructing complex types:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type StringResult = UnwrapPromise<Promise<string>>; // string
type NumberResult = UnwrapPromise<number>; // number
Nested infer
can handle more complex scenarios. For example, extracting the resolved type of a Promise returned by a function:
type UnwrapPromiseReturn<T> =
T extends (...args: any[]) => Promise<infer U> ? U :
T extends (...args: any[]) => infer V ? V :
never;
type A = UnwrapPromiseReturn<() => Promise<number>>; // number
type B = UnwrapPromiseReturn<() => string>; // string
Disabling Distributive Behavior
Sometimes, distributive behavior needs to be disabled. This can be achieved by wrapping the checked type in a tuple:
type NonDistributive<T> = [T] extends [any] ? true : false;
// Non-distributive behavior
type Test1 = NonDistributive<string | number>; // true
// Compared to distributive behavior
type Distributive<T> = T extends any ? true : false;
type Test2 = Distributive<string | number>; // true | true
Practical Application Examples
Conditional types are practical in React prop type handling. For example, creating a utility that dynamically determines the children
type based on component props
:
type PropsWithChildren<P> =
'children' extends keyof P
? P
: P & { children?: React.ReactNode };
function createComponent<P>(props: PropsWithChildren<P>) {
// Component implementation
}
// Automatically infers children type
createComponent({ value: 1 }); // children is optional
createComponent({ children: <div/>, value: 1 }); // children already exists
Another typical application is handling Redux action types:
type Action<T extends string, P = void> = {
type: T;
payload: P;
};
type ExtractAction<A, T> = A extends Action<infer U, infer P>
? U extends T
? P
: never
: never;
type MyActions =
| Action<'ADD', number>
| Action<'REMOVE', string>;
type AddPayload = ExtractAction<MyActions, 'ADD'>; // number
type RemovePayload = ExtractAction<MyActions, 'REMOVE'>; // string
Recursive Conditional Types
TypeScript 4.1 introduced recursive conditional types, allowing self-referential types. This makes handling nested structures possible:
type DeepReadonly<T> =
T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type NestedData = {
a: number;
b: {
c: string;
d: boolean[];
};
};
type ReadonlyData = DeepReadonly<NestedData>;
/* Equivalent to:
{
readonly a: number;
readonly b: {
readonly c: string;
readonly d: readonly boolean[];
};
}
*/
Recursive conditional types can also implement array operations at the type level, such as reversing a tuple:
type Reverse<T extends any[]> =
T extends [infer First, ...infer Rest]
? [...Reverse<Rest>, First]
: [];
type Numbers = [1, 2, 3, 4];
type Reversed = Reverse<Numbers>; // [4, 3, 2, 1]
Conditional Types and Template Literal Types
Combined with TypeScript 4.1's template literal types, conditional types can achieve more powerful string operations:
type ExtractRouteParams<T> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type Params = ExtractRouteParams<'/user/:id/posts/:postId'>;
/* Type is:
{
id: string;
postId: string;
}
*/
Performance Considerations
Complex conditional types may slow down type checking, especially with deep recursion. Optimization strategies include:
- Avoid excessive recursion depth.
- Use tail recursion optimization (TypeScript 4.5+).
- Pre-filter large union types.
// Tail recursion optimization example
type TrimLeft<T extends string> =
T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;
type Trimmed = TrimLeft<' hello'>; // "hello"
Combining Conditional Types with Mapped Types
Combining with mapped types enables more flexible type transformation utilities. For example, implementing a tool to wrap all method return types of an interface in Promises:
type Promisify<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[K];
};
interface Api {
getUser(id: number): { name: string };
checkStatus(): boolean;
}
type AsyncApi = Promisify<Api>;
/* Equivalent to:
{
getUser(id: number): Promise<{ name: string }>;
checkStatus(): Promise<boolean>;
}
*/
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn