Application of conditional types
Basic Concepts of Conditional Types
Conditional types in TypeScript allow us to select different types based on the relationship between input types. They take the form T extends U ? X : Y
, similar to the ternary operator in JavaScript. When type T
is assignable to type U
, the resulting type is X
; otherwise, it is Y
.
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
Conditional types are particularly useful in generics, where they can dynamically determine the resulting type based on type parameters. They are often combined with the infer
keyword to extract partial types from complex types.
Distributive Conditional Types
When conditional types operate on union types, they exhibit "distributive" behavior. This means the conditional type is distributed over each member of the union.
type ToArray<T> = T extends any ? T[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// Equivalent to string[] | number[]
This feature makes it convenient to handle union types. To prevent distributive behavior, you can wrap both sides of extends
in square brackets:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Arr = ToArrayNonDist<string | number>;
// Equivalent to (string | number)[]
Type Inference and the infer
Keyword
The infer
keyword allows us to declare a type variable within a conditional type to capture the type to be inferred. This is particularly useful for extracting parts of complex types.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function foo() { return 42; }
type FooReturn = ReturnType<typeof foo>; // number
infer
can be used in various type positions, including function parameters, array elements, Promise resolution values, etc.:
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type Num = UnpackPromise<Promise<number>>; // number
Built-in Utility Conditional Types
TypeScript provides several built-in conditional type utilities, which are themselves implemented using conditional types:
Exclude<T, U>
- Excludes from T those types that are assignable to U.
type T = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
Extract<T, U>
- Extracts from T those types that are assignable to U.
type T = Extract<"a" | "b" | 1 | 2, string>; // "a" | "b"
NonNullable<T>
- Excludes null and undefined from T.
type T = NonNullable<string | null | undefined>; // string
Recursive Conditional Types
Conditional types can reference themselves recursively, enabling the handling of nested structures or recursive type transformations:
type DeepReadonly<T> =
T extends object ? { readonly [P in keyof T]: DeepReadonly<T[P]> } : T;
type Nested = {
a: number;
b: {
c: boolean;
d: {
e: string;
};
};
};
type ReadonlyNested = DeepReadonly<Nested>;
Recursive conditional types are powerful for deeply nested data structures but require attention to recursion depth limits.
Combining Conditional Types with Mapped Types
Conditional types can be combined with mapped types to create more flexible type transformations:
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
type Person = {
name: string;
age: number;
greet: () => void;
};
type PersonMethods = FunctionPropertyNames<Person>; // "greet"
This combination can filter properties of specific types or apply different transformations to properties of different types.
Type Constraints in Conditional Types
In conditional types, we can add additional constraints to type parameters:
type Flatten<T> = T extends Array<infer U> ? U : T;
type StringArray = Array<string>;
type StringItem = Flatten<StringArray>; // string
This pattern is particularly useful when dealing with types that may or may not be arrays.
Conditional Types and Template Literal Types
Template literal types introduced in TypeScript 4.1 can be combined with conditional types:
type GetterName<T extends string> = `get${Capitalize<T>}`;
type NameGetter = GetterName<'name'>; // "getName"
type PropType<T, K extends string> =
K extends `on${infer Event}` ? (e: Event) => void : T[K];
type Props = {
onClick: (e: MouseEvent) => void;
value: string;
};
type ClickHandler = PropType<Props, 'onClick'>; // (e: MouseEvent) => void
This combination is particularly useful for handling event handlers or APIs with specific naming conventions.
Applications of Conditional Types in Complex Type Operations
Conditional types can be used to build complex type operations, such as type-safe Redux reducers:
type Action<T extends string = string, P = any> = {
type: T;
payload: P;
};
type ActionCreator<A extends Action> = (
payload: A['payload']
) => A;
type Reducer<S = any, A extends Action = Action> = (
state: S,
action: A
) => S;
type Handlers<S, A extends Action> = {
[T in A['type']]: Reducer<S, Extract<A, { type: T }>>
};
function createReducer<S, A extends Action>(
initialState: S,
handlers: Handlers<S, A>
): Reducer<S, A> {
return (state = initialState, action) => {
const handler = handlers[action.type];
return handler ? handler(state, action) : state;
};
}
This example demonstrates how to use conditional types to create type-safe Redux reducers, ensuring that action types match their corresponding handlers.
Conditional Types and Function Overloads
Conditional types can simplify the type definitions of function overloads:
type Overloads = {
(x: string): number;
(x: number): string;
};
type ReturnTypeByParam<T> =
T extends string ? number :
T extends number ? string :
never;
function overloaded<T extends string | number>(x: T): ReturnTypeByParam<T>;
function overloaded(x: any): any {
return typeof x === 'string' ? x.length : x.toString();
}
const a = overloaded('hello'); // number
const b = overloaded(42); // string
This approach is more concise than traditional function overloads, especially when there are many overloads.
Conditional Types and Recursive Type Limits
TypeScript imposes limits on recursive type depth, which may require special techniques when dealing with deep recursion:
type MaxDepth = 5; // Prevent infinite recursion
type DeepPartial<T, Depth extends number = 0> =
Depth extends MaxDepth ? T :
T extends object ? {
[P in keyof T]?: DeepPartial<T[P], AddOne<Depth>>;
} : T;
type AddOne<N extends number> =
[1, 2, 3, 4, 5, 6][N]; // Simple number increment
interface ComplexObject {
a: {
b: {
c: {
d: {
e: string;
};
};
};
};
}
type PartialComplex = DeepPartial<ComplexObject>;
This example shows how to limit recursion depth to prevent the type checker from entering infinite recursion.
Conditional Types and Type Predicates
Conditional types can be combined with type predicates to create more precise type guards:
type IsArray<T> = T extends any[] ? true : false;
function filterArray<T>(items: T[] | T): IsArray<T> extends true ? T[] : T {
return Array.isArray(items) ? items : [items] as any;
}
const arr = filterArray([1, 2, 3]); // number[]
const single = filterArray(42); // number
Although type assertions are needed, this pattern can provide better type safety in complex scenarios.
Conditional Types and Variadic Tuple Types
Variadic tuple types introduced in TypeScript 4.0 can be combined with conditional types:
type Shift<T extends any[]> =
T extends [infer First, ...infer Rest] ? Rest : [];
type Tuple = [string, number, boolean];
type Rest = Shift<Tuple>; // [number, boolean]
type Last<T extends any[]> =
T extends [...infer _, infer Last] ? Last : never;
type LastItem = Last<Tuple>; // boolean
These utility types are particularly useful when working with tuples, especially in the context of function parameter lists.
Conditional Types and Branded Types
Conditional types can be used to create and handle branded types:
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;
type IsBrand<T, B> = T extends Brand<infer U, B> ? true : false;
type A = IsBrand<UserId, 'UserId'>; // true
type B = IsBrand<PostId, 'UserId'>; // false
Branded types help distinguish between semantically different but structurally identical types, while conditional types can inspect and handle these branded types.
Conditional Types and Type-Level Programming
Conditional types enable complex type-level programming, such as type arithmetic:
type Length<T extends any[]> =
T extends { length: infer L } ? L : never;
type Tuple = [string, number, boolean];
type Len = Length<Tuple>; // 3
type BuildTuple<L extends number, T extends any[] = []> =
T['length'] extends L ? T : BuildTuple<L, [...T, any]>;
type Tuple3 = BuildTuple<3>; // [any, any, any]
Although TypeScript's type system is not Turing-complete, conditional types allow us to implement many useful type-level computations.
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:自定义工具类型
下一篇:infer关键字与类型提取