条件类型分发
00:00
分布式条件类型与控制
概述
条件类型分发(Distributive Conditional Type)是 TypeScript 条件类型在处理联合类型时的特殊行为:当条件类型的检查参数是裸类型参数(naked type parameter)时,TypeScript 会将联合类型的每个成员分别代入条件类型,然后将结果合并为新的联合类型。理解分发行为对于正确使用条件类型至关重要,因为有时需要利用分发来实现类型过滤和映射,有时又需要阻止分发以保持联合类型的完整性。
基础概念
分布式条件类型:当条件类型的形式为 T extends U ? X : Y,且 T 是裸类型参数(没有被元组、函数等包裹)时,TypeScript 会对 T 的每个联合成员分别求值。
裸类型参数:直接出现在条件类型 extends 左侧的类型参数,没有被其他类型构造器包裹。例如 T extends U 中的 T 是裸类型参数,而 [T] extends [U] 中的 T 不是。
阻止分发:将类型参数用元组包裹([T] extends [U])可以阻止分发行为,使联合类型作为整体参与条件判断。
never 的特殊行为:当分发的目标类型是 never 时,结果也是 never,因为对空集的任何映射结果都是空集。
快速上手
分发行为演示
// 分布式条件类型:对联合类型每个成员分别求值
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// 等价于:ToArray<string> | ToArray<number>
// 结果:string[] | number[]
// 对比:阻止分发
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNoDistribute<string | number>;
// 结果:(string | number)[]
never 的行为
// never 不触发分发,直接返回 never
type NeverTest = ToArray<never>; // never
// 原因:never 是空联合类型,没有成员可以分发
// 对空集的映射结果仍然是空集
详细用法
类型过滤
// 从联合类型中过滤出符合条件的成员
type Filter<T, U> = T extends U ? T : never;
// 只保留字符串类型
type OnlyStrings = Filter<string | number | boolean, string>;
// string
// 只保留函数类型
type OnlyFunctions = Filter<string | number | (() => void) | object, Function>;
// () => void
// 排除 null 和 undefined
type NonNull<T> = Filter<T, null | undefined>;
// 等价于 NonNullable<T>
类型映射
// 将联合类型中的某些成员替换为其他类型
type MapType<T, U, V> = T extends U ? V : T;
// 将 number 替换为 null
type ReplaceNumber = MapType<string | number | boolean, number, null>;
// string | null | boolean
// 将所有函数替换为字符串描述
type DescribeFunctions<T> = T extends (...args: any[]) => any
? `函数: ${T extends (...args: any[]) => infer R ? string : never}`
: T;
控制分发
// 阻止分发:使用元组包裹
type NoDistribute<T> = [T] extends [never] ? true : false;
type A = NoDistribute<string | number>; // false(联合类型不是 never)
type B = NoDistribute<never>; // true(never 是 never)
// 只对非 never 分发
type Wrap<T> = [T] extends [never] ? never : T extends any ? { value: T } : never;
type C = Wrap<string | number>;
// { value: string } | { value: number }
type D = Wrap<never>; // never
检测联合类型
// 判断一个类型是否为联合类型
type IsUnion<T> = [T] extends [never]
? false
: T extends any
? [T] extends [T]
? false
: true
: never;
// 原理解析:
// 1. 先排除 never
// 2. 利用分发的特性:如果 T 是联合类型,分发后 [T] extends [T] 会变成
// [A] extends [A | B] => false
// 3. 如果 T 不是联合类型,[T] extends [T] => true
type Test1 = IsUnion<string>; // false
type Test2 = IsUnion<string | number>; // true
type Test3 = IsUnion<never>; // false
常见场景
API 响应类型处理
// 根据成功/失败分发不同的响应类型
type ApiResponse<T, E = string> = { success: true; data: T } | { success: false; error: E };
// 提取成功时的数据类型
type ExtractData<T> = T extends { success: true; data: infer D } ? D : never;
type UserData = ExtractData<ApiResponse<{ id: number; name: string }>>;
// { id: number; name: string }
// 提取失败时的错误类型
type ExtractError<T> = T extends { success: false; error: infer E } ? E : never;
type UserError = ExtractError<ApiResponse<{ id: number }, { code: number; message: string }>>;
// { code: number; message: string }
递归展开
// 递归展开嵌套数组
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Nested = string[][][];
type Flat = Flatten<Nested>; // string
// 展开对象中的 Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type DeepPromise = Promise<Promise<Promise<number>>>;
type Unwrapped = UnwrapPromise<DeepPromise>; // number
条件类型与映射类型结合
// 只为可选属性生成 undefined 检查
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
// 为可选属性添加显式 undefined
type ExplicitUndefined<T> = {
[K in keyof T]: K extends OptionalKeys<T> ? T[K] | undefined : T[K];
};
interface Config {
name: string;
timeout?: number;
retries?: number;
}
type ExplicitConfig = ExplicitUndefined<Config>;
// { name: string; timeout: number | undefined; retries: number | undefined }
注意事项
- 裸类型参数:只有裸类型参数(直接出现在 extends 左侧的 T)才会触发分发。被
[T]、{ value: T }等包裹的 T 不会分发。 - never 的特殊性:never 作为联合类型时不会触发分发,因为它是空集。这可能导致条件类型的意外行为,需要用
[T] extends [never]单独处理。 - 性能影响:分发行为会增加类型检查的复杂度。对于大型联合类型(超过 10 个成员),分发可能导致编译变慢。
- 与映射类型的交互:映射类型中的条件类型也会触发分发。如果不需要分发,在映射类型内部也应使用元组包裹。
进阶用法
类型级编程
// 使用条件类型分发实现类型级布尔运算
type And<A extends boolean, B extends boolean> = A extends true
? B extends true
? true
: false
: false;
type Or<A extends boolean, B extends boolean> = A extends true
? true
: B extends true
? true
: false;
type Not<A extends boolean> = A extends true ? false : true;
type TestAnd = And<true, true>; // true
type TestOr = Or<false, true>; // true
type TestNot = Not<true>; // false
联合类型排列组合
// 生成两个联合类型的排列组合
type Combine<A, B> = A extends any ? (B extends any ? [A, B] : never) : never;
type Pair = Combine<'a' | 'b', 1 | 2>;
// ['a', 1] | ['a', 2] | ['b', 1] | ['b', 2]
// 生成对象键值对
type ObjectFromEntries<T extends [string, any]> = {
[K in T[0]]: Extract<T, [K, any]>[1];
};
type Entries = ['name', string] | ['age', number] | ['active', boolean];
type Obj = ObjectFromEntries<Entries>;
// { name: string; age: number; active: boolean }
条件类型链
// 多层条件类型链,类似模式匹配
type Match<T> = T extends string
? { type: 'string'; value: T }
: T extends number
? { type: 'number'; value: T }
: T extends boolean
? { type: 'boolean'; value: T }
: T extends Array<any>
? { type: 'array'; value: T }
: T extends object
? { type: 'object'; value: T }
: { type: 'unknown'; value: T };
type StringMatch = Match<'hello'>; // { type: 'string'; value: 'hello' }
type NumberMatch = Match<42>; // { type: 'number'; value: 42 }
type ArrayMatch = Match<string[]>; // { type: 'array'; value: string[] }