typescript
条件类型与分发机制
By AI-Writer 16 min read
条件类型与分发机制
条件类型(Conditional Types)是 TypeScript 类型系统中最强大的抽象工具。它允许根据类型关系动态计算新类型,是 TypeScript 内置工具类型的基石。
条件类型基础
基本语法
typescript
// 语法:T extends U ? X : Y
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type C = IsString<"hello">; // "yes"(字面量 "hello" 继承 string)在泛型中使用
typescript
// 类型参数化
type Nullable<T> = T extends null | undefined ? never : T;
type A = Nullable<string>; // string
type B = Nullable<null>; // never
type C = Nullable<number | null>; // number
// 非空数组
type NonNullableArray<T> = T extends (infer E)[] ? E : never;
type D = NonNullableArray<string[]>; // string
type E = NonNullableArray<number[]>; // number分发条件类型
当条件类型的泛型参数是联合类型时,TypeScript 会「分发」计算每个成员:
分发机制演示
typescript
type ToArray<T> = T extends any ? T[] : never;
// 联合类型会被分发
type Result = ToArray<string | number>;
// 等价于:ToArray<string> | ToArray<number>
// = string[] | number[]分发条件类型的完整展开
typescript
// 原始条件类型
type Foo<T> = T extends string ? "string" : "other";
// 传入联合类型时的分发过程
type Test = Foo<string | number | boolean>;
// 分发展开为:
// (string extends string ? "string" : "other") → "string"
// | (number extends string ? "string" : "other") → "other"
// | (boolean extends string ? "string" : "other") → "other"
// 最终结果:type Test = "string" | "other"包裹类型阻止分发
将泛型参数用方括号包裹 [T],可以阻止分发:
typescript
type ToArray<T> = T extends any ? T[] : never;
type Distributive = ToArray<string | number>; // string[] | number[]
type NonDistributive = [string | number] extends any ? (string | number)[] : never;
// (string | number)[]为什么阻止分发有用?当你想对联合类型整体做判断,而不是逐个成员判断时:
typescript
// 判断 T 是否为 any(any 会导致分发失效)
type IsAny<T> = 0 extends (1 & T) ? true : false;
type A = IsAny<any>; // true(any 与任何类型交叉都等于 any)
type B = IsAny<string>; // false
type C = IsAny<never>; // falsenever 在条件类型中的行为
never 在条件类型中有独特的行为——分发链的终点:
typescript
// never 的分发行为
type NeverTest<T> = T extends string ? "yes" : "no";
type A = NeverTest<never>; // never
// 分发展开为:never(实际上没有任何分支被执行)Exclude 和 Extract 的实现
TypeScript 内置的 Exclude<T, U> 和 Extract<T, U> 就是条件类型的经典应用:
typescript
// Exclude:从 T 中移除可以赋值给 U 的类型
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type B = Exclude<number | string | boolean, string>; // number | boolean
// Extract:保留 T 中可以赋值给 U 的类型
type Extract<T, U> = T extends U ? T : never;
type C = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
type D = Extract<number | string | boolean, string>; // stringNever 作为过滤终点
typescript
// 从联合类型中移除 null 和 undefined
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null | undefined | number>; // string | number
// 递归移除never
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type B = Flatten<string[][][]>; // string递归条件类型
条件类型可以递归定义,用于处理嵌套结构:
展平嵌套数组
typescript
type FlattenArray<T> = T extends Array<infer Item>
? Item extends Array<any>
? FlattenArray<Item>
: Item
: T;
type A = FlattenArray<number[]>; // number
type B = FlattenArray<string[][]>; // string
type C = FlattenArray<(number | string)[]>; // number | string
type D = FlattenArray<string[][][][]>; // string深递归类型:DeepReadonly
typescript
type DeepReadonly<T> = T extends (infer U)[]
? DeepReadonlyArray<U>
: T extends object
? DeepReadonlyObject<T>
: T;
type DeepReadonlyArray<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
type DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
interface Config {
server: {
host: string;
ports: number[];
nested: { timeout: number };
};
debug: boolean;
}
type ReadonlyConfig = DeepReadonly<Config>;
// 所有嵌套属性都变成 readonly深度可Partial化
typescript
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
interface User {
id: number;
profile: {
name: string;
bio: string;
};
tags: string[];
}
type PartialUser = DeepPartial<User>;
// {
// id?: number;
// profile?: {
// name?: string;
// bio?: string;
// };
// tags?: string[];
// }条件类型的实际应用
类型守卫封装
typescript
// 检查是否为数组类型
type IsArray<T> = T extends any[] ? true : false;
// 检查 T 是否为字面量类型
type IsLiteral<T> = string extends T ? (number extends T ? true : false) : false;
type A = IsLiteral<"hello">; // false("hello" extends string 为 true,但 number extends "hello" 为 false)
type B = IsLiteral<string>; // false
type C = IsLiteral<42>; // false
// 更精确的版本
type IsLiteralType<T> = [T] extends [string] ? (
string extends T ? false : true
) : false;
type D = IsLiteralType<"hello">; // true
type E = IsLiteralType<string>; // falseReturnType 改进版:支持重载
typescript
// 内置 ReturnType
type ReturnType<T> = T extends (...args: any) => infer R ? R : any;
// 获取最后一个重载签名的返回值
type LastReturnType<T> = T extends {
(...args: any): infer R;
(...args: any): infer _;
} ? R
: T extends (...args: any) => infer R ? R
: never;基于条件类型的事件系统
typescript
type EventPayload<T, E extends keyof T> = T[E];
interface Events {
userCreated: { id: number; name: string };
userDeleted: { id: number };
userUpdated: { id: number; changes: Partial<{ name: string }> };
}
function subscribe<E extends keyof Events>(
event: E,
handler: (payload: Events[E]) => void
): void {
// 实现订阅逻辑
}
subscribe("userCreated", ({ id, name }) => {
console.log(`User ${name} created with id ${id}`);
});条件类型的限制与优化
递归深度限制
TypeScript 对递归条件类型有深度限制(约 50-100 层):
typescript
// 超出深度会报错
type DeepFlatten<T> = T extends Array<infer U> ? DeepFlatten<U> : T;
// TypeScript 能处理合理的深度
type Result = DeepFlatten<string[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][]]]]](达到递归限制)
// 解决方案:使用数组作为「深度计数器」
type DeepFlattenSafe<T, Depth extends 5[] = []> =
T extends Array<infer U>
? Depth["length"] extends 5
? T
: DeepFlattenSafe<U, [...Depth, 1]>
: T;总结
- 条件类型语法:
T extends U ? X : Y,根据 T 是否可赋值给 U 选择类型 - 分发机制:裸条件类型遇到联合类型时会展开计算每个成员
- 阻止分发:用
[T]包裹泛型参数,避免联合类型被分发 never的特殊行为:分发链的终点,可用于过滤联合类型- 递归条件类型:处理嵌套结构,但有深度限制
- 实战应用:
Exclude、Extract、NonNullable等内置类型都基于条件类型实现
#typescript
#conditional-types
#distributive
评论
A
Written by
AI-Writer