TypeScript 类型体操实战:从 infer 到递归类型的高级技巧
TypeScript 类型体操实战:从 infer 到递归类型的高级技巧
TypeScript 的类型系统是图灵完备的——这意味着你可以在类型层面做几乎任何计算。这听起来很学术,但实际上,掌握高级类型技巧能让你写出更安全、更具表达力的代码。本文将从条件类型出发,逐步深入 infer、模板字面量类型、递归类型和类型分发,最终带你手写一系列实用的工具类型。
一、条件类型:类型系统的 if-else
条件类型是一切高级类型技巧的基石。语法很简单:
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // truetype B = IsString<42>; // falseT extends U ? X : Y 的本质是:如果 T 可以赋值给 U,则结果为 X,否则为 Y。
1.1 条件类型的分发特性
当条件类型作用于联合类型时,会自动分发(distribute):
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;// 等价于 ToArray<string> | ToArray<number>// 即 string[] | number[]注意,这里得到的是 string[] | number[],而不是 (string | number)[]。如果你想要后者,需要用方括号包裹来阻止分发:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;// (string | number)[]1.2 利用分发实现类型过滤
分发特性可以用来从联合类型中过滤出特定类型:
type ExtractString<T> = T extends string ? T : never;
type Filtered = ExtractString<'a' | 1 | 'b' | true>;// 'a' | 'b'这就是内置 Extract 和 Exclude 的原理:
// 内置实现type Extract<T, U> = T extends U ? T : never;type Exclude<T, U> = T extends U ? never : T;
type OnlyNumbers = Extract<string | number | boolean, number>;// number
type WithoutString = Exclude<string | number | boolean, string>;// number | boolean二、infer 关键字:类型层面的模式匹配
infer 是条件类型中最强大的武器。它允许你在 extends 子句中声明一个待推断的类型变量。
2.1 基础用法:提取函数返回类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (x: number, y: string) => boolean;type R = MyReturnType<Fn>; // booleaninfer R 的意思是:如果 T 能匹配函数签名,就把返回值类型”捕获”到 R 中。
2.2 提取函数参数类型
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type Params = MyParameters<(a: number, b: string) => void>;// [a: number, b: string]2.3 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // stringtype B = UnwrapPromise<number>; // number2.4 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type El = ElementOf<string[]>; // stringtype El2 = ElementOf<[1, 'a', true]>; // 1 | 'a' | true2.5 infer 在元组中的妙用
提取元组的第一个和剩余元素:
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;type Rest<T extends any[]> = T extends [any, ...infer R] ? R : never;type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type F = First<[1, 2, 3]>; // 1type R = Rest<[1, 2, 3]>; // [2, 3]type L = Last<[1, 2, 3]>; // 3三、模板字面量类型:字符串级别的类型运算
TypeScript 4.1 引入的模板字面量类型,让你可以在类型层面做字符串操作。
3.1 基础拼接
type Greeting<T extends string> = `Hello, ${T}!`;
type G = Greeting<'World'>; // "Hello, World!"3.2 结合联合类型生成排列组合
type Color = 'red' | 'blue' | 'green';type Size = 'sm' | 'md' | 'lg';
type ClassName = `${Color}-${Size}`;// "red-sm" | "red-md" | "red-lg" | "blue-sm" | "blue-md" | "blue-lg" | "green-sm" | "green-md" | "green-lg"3.3 结合 infer 做字符串模式匹配
// 提取路由参数type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractParams<Rest> : T extends `${string}:${infer Param}` ? Param : never;
type Params = ExtractParams<'/api/:version/users/:id/posts/:postId'>;// "version" | "id" | "postId"3.4 实现 CamelCase 转换
type CamelCase<S extends string> = S extends `${infer Head}-${infer Tail}` ? `${Head}${CamelCase<Capitalize<Tail>>}` : S;
type Camel = CamelCase<'hello-world-foo-bar'>;// "helloWorldFooBar"3.5 内置字符串工具类型
TypeScript 内置了四个字符串操作类型:
type A = Uppercase<'hello'>; // "HELLO"type B = Lowercase<'HELLO'>; // "hello"type C = Capitalize<'hello'>; // "Hello"type D = Uncapitalize<'Hello'>; // "hello"四、递归类型:类型系统中的循环
递归类型让你可以处理任意深度的嵌套结构。
4.1 深度 Readonly
内置的 Readonly 只处理一层,我们来实现深度版本:
type DeepReadonly<T> = T extends Function ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
interface Nested { a: { b: { c: string; d: number[]; }; }; fn: () => void;}
type ReadonlyNested = DeepReadonly<Nested>;// {// readonly a: {// readonly b: {// readonly c: string;// readonly d: readonly number[];// };// };// readonly fn: () => void;// }4.2 深度 Partial
type DeepPartial<T> = T extends Function ? T : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
interface Config { server: { host: string; port: number; ssl: { cert: string; key: string; }; }; debug: boolean;}
type PartialConfig = DeepPartial<Config>;// 所有层级的属性都变为可选4.3 递归展平元组
type Flatten<T extends any[]> = T extends [infer First, ...infer Rest] ? First extends any[] ? [...Flatten<First>, ...Flatten<Rest>] : [First, ...Flatten<Rest>] : [];
type Flat = Flatten<[1, [2, [3, 4]], [5]]>;// [1, 2, 3, 4, 5]4.4 递归实现 Awaited(深度解包 Promise)
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type A = DeepAwaited<Promise<Promise<Promise<string>>>>;// string这就是 TypeScript 内置 Awaited 类型的核心原理。
4.5 类型层面的数字运算
利用元组长度可以实现类型级别的加法:
type BuildTuple<N extends number, T extends any[] = []> = T['length'] extends N ? T : BuildTuple<N, [...T, any]>;
type Add<A extends number, B extends number> = [...BuildTuple<A>, ...BuildTuple<B>]['length'];
type Sum = Add<3, 4>; // 7
type Subtract<A extends number, B extends number> = BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest] ? Rest['length'] : never;
type Diff = Subtract<10, 3>; // 7五、实战工具类型
掌握了上面的基础之后,来实现一些实际开发中有用的工具类型。
5.1 PathKeys:获取对象所有嵌套路径
type PathKeys<T, Prefix extends string = ''> = T extends object ? { [K in keyof T & string]: | `${Prefix}${K}` | PathKeys<T[K], `${Prefix}${K}.`> }[keyof T & string] : never;
interface User { name: string; address: { city: string; geo: { lat: number; lng: number; }; }; tags: string[];}
type UserPaths = PathKeys<User>;// "name" | "address" | "address.city" | "address.geo" | "address.geo.lat" | "address.geo.lng" | "tags"5.2 PathValue:根据路径获取对应类型
type PathValue<T, P extends string> = P extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? PathValue<T[Key], Rest> : never : P extends keyof T ? T[P] : never;
type CityType = PathValue<User, 'address.city'>; // stringtype LatType = PathValue<User, 'address.geo.lat'>; // number5.3 类型安全的 EventEmitter
type EventMap = { click: { x: number; y: number }; focus: { target: string }; resize: { width: number; height: number };};
type TypedEmitter<Events extends Record<string, any>> = { on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): void; emit<K extends keyof Events>(event: K, payload: Events[K]): void; off<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): void;};
// 使用declare const emitter: TypedEmitter<EventMap>;emitter.on('click', (payload) => { // payload 自动推断为 { x: number; y: number } console.log(payload.x, payload.y);});
emitter.emit('resize', { width: 100, height: 200 }); // ✅// emitter.emit('resize', { width: 100 }); // ❌ 缺少 height5.4 实现 PickByValue
根据值的类型来筛选属性:
type PickByValue<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K]};
interface Mixed { name: string; age: number; active: boolean; email: string;}
type StringProps = PickByValue<Mixed, string>;// { name: string; email: string }5.5 实现 MutableKeys 和 ReadonlyKeys
type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? A : B;
type ReadonlyKeys<T> = { [K in keyof T]-?: IfEquals< { [Q in K]: T[K] }, { -readonly [Q in K]: T[K] }, never, K >}[keyof T];
type MutableKeys<T> = { [K in keyof T]-?: IfEquals< { [Q in K]: T[K] }, { -readonly [Q in K]: T[K] }, K, never >}[keyof T];
interface Example { readonly id: number; name: string; readonly createdAt: Date; email: string;}
type RK = ReadonlyKeys<Example>; // "id" | "createdAt"type MK = MutableKeys<Example>; // "name" | "email"5.6 实现 UnionToIntersection
将联合类型转为交叉类型,这是一个经典的类型体操题:
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
type Union = { a: string } | { b: number } | { c: boolean };type Intersection = UnionToIntersection<Union>;// { a: string } & { b: number } & { c: boolean }原理分析:
U extends any ? (x: U) => void : never— 利用分发特性,将联合类型的每个成员包装成函数参数- 结果是
((x: A) => void) | ((x: B) => void) | ((x: C) => void) - 当这个联合函数类型被
infer推断参数时,TypeScript 会取参数的交叉类型(逆变位置的联合 → 交叉)
5.7 实现 UnionToTuple
这是类型体操的终极 Boss 之一:
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
type LastOfUnion<T> = UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never;
type UnionToTuple<T, Acc extends any[] = []> = [T] extends [never] ? Acc : UnionToTuple<Exclude<T, LastOfUnion<T>>, [LastOfUnion<T>, ...Acc]>;
type Tuple = UnionToTuple<'a' | 'b' | 'c'>;// ['a', 'b', 'c']六、类型体操的实际应用场景
6.1 类型安全的 API 路由定义
type ExtractRouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractRouteParams<Rest>]: string } : T extends `${string}:${infer Param}` ? { [K in Param]: string } : {};
function defineRoute<T extends string>( path: T, handler: (params: ExtractRouteParams<T>) => void) { // ...}
defineRoute('/users/:id/posts/:postId', (params) => { // params: { id: string; postId: string } console.log(params.id, params.postId);});6.2 类型安全的 SQL Builder
type Column<Table extends Record<string, any>> = keyof Table & string;
type SelectResult< Table extends Record<string, any>, Cols extends Column<Table>[]> = Pick<Table, Cols[number]>;
interface UserTable { id: number; name: string; email: string; age: number;}
function select<T extends Record<string, any>>() { return { columns<C extends Column<T>[]>(...cols: C) { return { from(table: string): SelectResult<T, C>[] { return [] as any; } }; } };}
const users = select<UserTable>().columns('name', 'email').from('users');// users: Pick<UserTable, "name" | "email">[]// 即 { name: string; email: string }[]七、性能与注意事项
7.1 递归深度限制
TypeScript 对递归类型有深度限制(大约 1000 层)。超过后会报错 Type instantiation is excessively deep and possibly infinite。
// 可能触发深度限制的写法type TooDeep<N extends number, T extends any[] = []> = T['length'] extends N ? T : TooDeep<N, [...T, any]>;
// type Huge = TooDeep<2000>; // ❌ 超过递归限制7.2 尾递归优化
TypeScript 4.5 引入了尾递归优化。确保递归调用在尾部位置:
// ✅ 尾递归 — 有优化type Reverse<T extends any[], Acc extends any[] = []> = T extends [infer F, ...infer R] ? Reverse<R, [F, ...Acc]> : Acc;
// ❌ 非尾递归 — 无优化type ReverseBad<T extends any[]> = T extends [infer F, ...infer R] ? [...ReverseBad<R>, F] : [];7.3 编译性能
复杂的类型体操会显著增加编译时间。在大型项目中要权衡类型安全和编译速度:
- 避免在热路径上使用深度递归类型
- 适当使用
any或as做类型断言 - 考虑把复杂类型提取为独立的
.d.ts文件
八、总结
TypeScript 类型体操不是炫技——它是让类型系统为你工作的方式。掌握这些技巧后,你可以:
- 写出更精确的类型定义:减少
any,让编译器帮你捕获 bug - 构建类型安全的工具库:API 路由、ORM、配置系统都能受益
- 理解开源库的类型定义:Vue、Prisma 等项目大量使用这些技巧
关键心法:
extends是判断,infer是提取,递归是循环- 联合类型 + 条件类型 = 自动分发
- 模板字面量 + infer = 字符串模式匹配
- 元组 + 递归 = 列表操作
建议从 type-challenges 开始练习,从 easy 到 hard 逐步提升。类型体操和算法一样,练得多了自然就熟了。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!