条件类型与类型体操
一句话概述:条件类型、分布式特性、infer 推断、递归类型——掌握 TypeScript 类型编程的核心武器
什么是类型体操?
定义:利用 TypeScript 类型系统的图灵完备特性,在类型层面进行逻辑运算、模式匹配和数据变换的编程技巧。
涉及场景:
- 库/框架开发:为用户提供精确的类型推断(如 Vue 的
defineComponent、tRPC 的端到端类型安全) - 工具类型封装:
DeepPartial、Path<T>等项目通用类型 - 面试考察:中高级前端必考,考察对类型系统的深入理解
核心工具:
- 条件类型(Conditional Types)
infer关键字- 映射类型(Mapped Types)
- 模板字面量类型(Template Literal Types)
- 递归类型(Recursive Types)
一、条件类型基础
语法
// T extends U ? X : Y
// 如果 T 可赋值给 U,结果为 X,否则为 Y
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
type C = IsString<string>; // true嵌套条件类型
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends undefined ? 'undefined' :
T extends Function ? 'function' :
'object';
type T1 = TypeName<string>; // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<string[]>; // "object"二、分布式条件类型
核心规则
当条件类型的 T 是一个裸类型参数(naked type parameter),且传入的是联合类型时,会自动分发到每个成员:
type ToArray<T> = T extends any ? T[] : never;
// 分发过程:
// ToArray<string | number>
// = (string extends any ? string[] : never) | (number extends any ? number[] : never)
// = string[] | number[]
type R1 = ToArray<string | number>; // string[] | number[]什么是"裸类型参数"?
// ✅ 裸类型参数:T 直接出现在 extends 左侧
type Naked<T> = T extends string ? 'yes' : 'no';
// ❌ 非裸:T 被包裹了
type Wrapped<T> = [T] extends [string] ? 'yes' : 'no';
// 区别
type R1 = Naked<string | number>; // "yes" | "no"(分发了)
type R2 = Wrapped<string | number>; // "no"(没分发,[string | number] 不能赋给 [string])never 的特殊行为
never 是空联合类型(zero members),分发后什么都不剩:
type Test<T> = T extends string ? 'yes' : 'no';
type R = Test<never>; // never(不是 "yes" 也不是 "no"!)
// 解决:用 [T] 包裹阻止分发
type IsNever<T> = [T] extends [never] ? true : false;
type R1 = IsNever<never>; // true ✅
type R2 = IsNever<string>; // false ✅利用分发实现过滤
// Exclude:从联合类型中排除
type Exclude<T, U> = T extends U ? never : T;
type R1 = Exclude<'a' | 'b' | 'c', 'a' | 'c'>; // "b"
// Extract:从联合类型中提取
type Extract<T, U> = T extends U ? T : never;
type R2 = Extract<string | number | boolean, string | number>; // string | number
// NonNullable:排除 null 和 undefined
type NonNullable<T> = T & {};
type R3 = NonNullable<string | null | undefined>; // string三、infer 关键字
基本用法
infer 在条件类型的 extends 子句中声明一个待推断的类型变量:
// 提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<(x: number) => boolean>; // boolean常见 infer 模式
// 1. 提取函数参数类型
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type P1 = Parameters<(a: string, b: number) => void>; // [a: string, b: number]
// 2. 提取第一个参数
type FirstParam<T> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F1 = FirstParam<(name: string, age: number) => void>; // string
// 3. 提取 Promise 内部类型(递归)
type Awaited<T> =
T extends Promise<infer U> ? Awaited<U> : T;
type A1 = Awaited<Promise<string>>; // string
type A2 = Awaited<Promise<Promise<number>>>; // number
// 4. 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E1 = ElementOf<string[]>; // string
type E2 = ElementOf<[1, 'a', true]>; // 1 | "a" | true
// 5. 提取构造函数实例类型
type InstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : any;infer 在协变和逆变位置的行为
// 协变位置(如返回值):多个候选取联合
type CovariantInfer<T> =
T extends { a: infer U; b: infer U } ? U : never;
type R1 = CovariantInfer<{ a: string; b: number }>; // string | number
// 逆变位置(如函数参数):多个候选取交叉
type ContravariantInfer<T> =
T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
type R2 = ContravariantInfer<{ a: (x: string) => void; b: (x: number) => void }>;
// string & number → never四、模板字面量类型
基础语法
type Greeting = `Hello, ${string}!`;
// 匹配所有 "Hello, xxx!" 格式的字符串
const g1: Greeting = 'Hello, World!'; // ✅
// const g2: Greeting = 'Hi, World!'; // ❌联合类型展开
// 自动展开为所有组合
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'large';
type ClassName = `${Size}-${Color}`;
// "small-red" | "small-green" | "small-blue" | "large-red" | "large-green" | "large-blue"内置字符串工具类型
type S1 = Uppercase<'hello'>; // "HELLO"
type S2 = Lowercase<'HELLO'>; // "hello"
type S3 = Capitalize<'hello'>; // "Hello"
type S4 = Uncapitalize<'Hello'>; // "hello"
// 常见应用:事件名生成
type EventName<T extends string> = `on${Capitalize<T>}`;
type E = EventName<'click' | 'focus'>; // "onClick" | "onFocus"模式匹配(infer + 模板字面量)
// 提取路由参数
type ExtractParams<S extends string> =
S extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: S extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<'/user/:id/post/:postId'>;
// "id" | "postId"
// CamelCase 转 kebab-case
type CamelToKebab<S extends string> =
S extends `${infer First}${infer Rest}`
? Rest extends Uncapitalize<Rest>
? `${Lowercase<First>}${CamelToKebab<Rest>}`
: `${Lowercase<First>}-${CamelToKebab<Rest>}`
: S;
type R = CamelToKebab<'backgroundColor'>; // "background-color"五、递归类型
深层操作
// DeepPartial:递归将所有属性变为可选
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// DeepReadonly:递归将所有属性变为只读
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// DeepRequired:递归将所有属性变为必选
type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;路径类型(对象所有嵌套路径)
type Path<T> = T extends object
? {
[K in keyof T & string]: K | `${K}.${Path<T[K]>}`
}[keyof T & string]
: never;
interface Config {
db: { host: string; port: number };
log: { level: string };
}
type ConfigPaths = Path<Config>;
// "db" | "db.host" | "db.port" | "log" | "log.level"
// 根据路径获取值类型
type PathValue<T, P extends string> =
P extends `${infer K}.${infer Rest}`
? K extends keyof T ? PathValue<T[K], Rest> : never
: P extends keyof T ? T[P] : never;
type HostType = PathValue<Config, 'db.host'>; // string元组操作
// 反转元组
type Reverse<T extends any[]> =
T extends [infer First, ...infer Rest]
? [...Reverse<Rest>, First]
: [];
type R1 = Reverse<[1, 2, 3]>; // [3, 2, 1]
// 扁平化元组
type Flatten<T extends any[]> =
T extends [infer First, ...infer Rest]
? First extends any[]
? [...Flatten<First>, ...Flatten<Rest>]
: [First, ...Flatten<Rest>]
: [];
type R2 = Flatten<[1, [2, [3]], 4]>; // [1, 2, 3, 4]
// 去重
type Unique<T extends any[], Seen extends any[] = []> =
T extends [infer First, ...infer Rest]
? First extends Seen[number]
? Unique<Rest, Seen>
: Unique<Rest, [...Seen, First]>
: Seen;
type R3 = Unique<[1, 2, 1, 3, 2]>; // [1, 2, 3]六、实战:经典类型体操题解
1. 实现 Pick
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
};2. 实现 Readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
};3. 实现 TupleToUnion
type TupleToUnion<T extends readonly any[]> = T[number];
type R = TupleToUnion<[1, 'a', true]>; // 1 | "a" | true4. 实现 Last(获取元组最后一个元素)
type Last<T extends any[]> =
T extends [...infer _, infer L] ? L : never;
type R = Last<[1, 2, 3]>; // 35. 实现 Trim
type Whitespace = ' ' | '\n' | '\t';
type TrimLeft<S extends string> =
S extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : S;
type TrimRight<S extends string> =
S extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : S;
type Trim<S extends string> = TrimLeft<TrimRight<S>>;
type R = Trim<' hello '>; // "hello"6. 实现 Replace
type Replace<
S extends string,
From extends string,
To extends string
> = From extends ''
? S
: S extends `${infer Head}${From}${infer Tail}`
? `${Head}${To}${Tail}`
: S;
type R = Replace<'hello world', 'world', 'TS'>; // "hello TS"7. 实现 UnionToIntersection
type UnionToIntersection<U> =
(U extends any ? (arg: U) => void : never) extends (arg: infer I) => void
? I
: never;
type R = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }8. 实现 IsEqual(精确判断两个类型是否相等)
type IsEqual<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
? true
: false;
type R1 = IsEqual<string, string>; // true
type R2 = IsEqual<string, number>; // false
type R3 = IsEqual<{ a: 1 }, { a: 1 }>; // true
type R4 = IsEqual<any, unknown>; // false七、性能注意事项
类型递归深度限制
TypeScript 对递归类型有约 50 层的深度限制,超过会报错:
// ❌ 可能超出递归限制
type DeepFlatten<T> = T extends object
? { [K in keyof T]: DeepFlatten<T[K]> }
: T;联合类型爆炸
模板字面量类型组合联合类型时,数量是乘法关系:
type A = 'a' | 'b' | 'c'; // 3
type B = '1' | '2' | '3'; // 3
type C = `${A}${B}`; // 9 种组合
// 如果每个有 100 个成员 → 10,000 种组合 → 编译极慢优化建议
- 优先使用
interface extends而非type &(interface 有缓存优化) - 避免不必要的深层递归:限制递归深度或使用尾递归优化
- 拆分大型条件类型:多个小条件比一个巨型条件更容易缓存
- 减少联合类型成员数量:模板字面量组合时注意爆炸
面试高频题
1. 分布式条件类型的原理是什么?怎么阻止分发?
答案:当条件类型 T extends U ? X : Y 中的 T 是裸类型参数,且传入联合类型时,会分发到每个成员分别计算再合并。阻止方法是用元组包裹:[T] extends [U],使 T 不再是裸类型参数。
2. infer 在协变和逆变位置的推断有什么区别?
答案:同一个 infer 变量在协变位置(如返回值、属性值)出现多次时,推断结果是联合类型;在逆变位置(如函数参数)出现多次时,推断结果是交叉类型。这是 UnionToIntersection 的实现原理。
3. 如何判断一个类型是否为 never?
答案:不能用 T extends never ? true : false,因为 never 作为空联合类型会触发分发,结果还是 never。正确做法是 [T] extends [never] ? true : false,用元组包裹阻止分发。
4. 类型体操有什么实际应用价值?
答案:主要用于库和框架开发,提供精确的类型推断。例如:Vue 的 defineComponent 自动推断 props/emits 类型、tRPC 的端到端类型安全、React Hook Form 的路径类型 Path<T>、Zod 的 z.infer<typeof schema> 等。日常业务开发中不需要写复杂类型体操,但理解原理有助于读懂框架类型定义和排查类型错误。