TypeScript 类型体操实战:从 infer 到递归类型的高级技巧

2692 字
13 分钟
TypeScript 类型体操实战:从 infer 到递归类型的高级技巧

TypeScript 类型体操实战:从 infer 到递归类型的高级技巧#

TypeScript 的类型系统是图灵完备的——这意味着你可以在类型层面做几乎任何计算。这听起来很学术,但实际上,掌握高级类型技巧能让你写出更安全、更具表达力的代码。本文将从条件类型出发,逐步深入 infer、模板字面量类型、递归类型和类型分发,最终带你手写一系列实用的工具类型。

一、条件类型:类型系统的 if-else#

条件类型是一切高级类型技巧的基石。语法很简单:

type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false

T 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'

这就是内置 ExtractExclude 的原理:

// 内置实现
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>; // boolean

infer 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>>; // string
type B = UnwrapPromise<number>; // number

2.4 提取数组元素类型#

type ElementOf<T> = T extends (infer E)[] ? E : never;
type El = ElementOf<string[]>; // string
type El2 = ElementOf<[1, 'a', true]>; // 1 | 'a' | true

2.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]>; // 1
type 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'>; // string
type LatType = PathValue<User, 'address.geo.lat'>; // number

5.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 }); // ❌ 缺少 height

5.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 }

原理分析

  1. U extends any ? (x: U) => void : never — 利用分发特性,将联合类型的每个成员包装成函数参数
  2. 结果是 ((x: A) => void) | ((x: B) => void) | ((x: C) => void)
  3. 当这个联合函数类型被 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 编译性能#

复杂的类型体操会显著增加编译时间。在大型项目中要权衡类型安全和编译速度:

  • 避免在热路径上使用深度递归类型
  • 适当使用 anyas 做类型断言
  • 考虑把复杂类型提取为独立的 .d.ts 文件

八、总结#

TypeScript 类型体操不是炫技——它是让类型系统为你工作的方式。掌握这些技巧后,你可以:

  1. 写出更精确的类型定义:减少 any,让编译器帮你捕获 bug
  2. 构建类型安全的工具库:API 路由、ORM、配置系统都能受益
  3. 理解开源库的类型定义:Vue、Prisma 等项目大量使用这些技巧

关键心法:

  • extends 是判断,infer 是提取,递归是循环
  • 联合类型 + 条件类型 = 自动分发
  • 模板字面量 + infer = 字符串模式匹配
  • 元组 + 递归 = 列表操作

建议从 type-challenges 开始练习,从 easy 到 hard 逐步提升。类型体操和算法一样,练得多了自然就熟了。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

TypeScript 类型体操实战:从 infer 到递归类型的高级技巧
https://boke.hackerdream.xyz/posts/typescript-type-gymnastics/
作者
晴天
发布于
2026-01-09
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录