递归类型
本教程共 80 篇 · 第 50 篇 · 更新于 2026-08-10 · 约 10 分钟阅读
本节目标:理解递归类型的自引用机制,学会用它表达树形结构、JSON 数据,并写出 DeepPartial、DeepReadonly 等深度工具类型。
递归类型是什么
递归类型就是在类型定义里引用自己的类型。它的典型场景是描述嵌套结构——文件夹套文件夹、列表套列表。
interface TreeNode {
value: string;
children?: TreeNode[]; // 引用自身
}
TreeNode 的 children 属性类型是 TreeNode[],这就形成了递归:每个节点可以有子节点,子节点还可以有子节点,理论上无限深。
为什么需要递归类型
TypeScript 内置的 Partial<T> 只做一层浅可选:
interface Config {
server: {
port: number;
host: string;
};
}
type ShallowConfig = Partial<Config>;
// server 属性变成了可选的,但 server 里的 port 和 host 还是必填的
如果你的配置是三层嵌套,Partial 只让你省掉第一层。要想所有层级都可选,就得递归。
实际应用
JSON 类型
这是递归类型最经典的用法——一个字面量 JSON 表达式可以包含任意嵌套:
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
这个类型精确表达了 JSON 规范的所有合法值。JSONValue[] 和 { [key: string]: JSONValue } 两行实现了递归——数组元素和对象属性都可以是 JSONValue 本身。
DeepPartial:深度可选
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
interface AppConfig {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
logging: {
level: "debug" | "info" | "error";
file?: string;
};
}
// 所有层级都变可选了
type PartialConfig = DeepPartial<AppConfig>;
const config: PartialConfig = {
database: {
host: "localhost"
// port, credentials 全部可选
}
// logging 也可选
};
拆解 DeepPartial 的逻辑:
T extends object:如果是对象,就遍历它的每个键[P in keyof T]?:每个属性变成可选DeepPartial<T[P]>:对属性值类型递归调用 DeepPartial- 如果是原始类型(string、number 等),直接返回 T
DeepReadonly:深度只读
同样的逻辑,把可选换成只读:
type DeepReadonly<T> = T extends Function
? T
: T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
interface User {
name: string;
profile: {
email: string;
address: {
city: string;
zip: string;
};
};
}
const user: DeepReadonly<User> = {
name: "张三",
profile: {
email: "zhangsan@test.com",
address: { city: "上海", zip: "200000" }
}
};
// user.name = "李四"; // ❌ 顶层只读
// user.profile.address.city = "北京"; // ❌ 深层也只读
注意 T extends Function 这个分支:函数类型也是 object,但我们不想把函数拆开,所以单独处理。
路径字符串类型(进阶)
递归类型还可以写出一套类型安全的”对象路径”类型:
type Path<T, K extends keyof T = keyof T> =
K extends string
? T[K] extends object
? K | `${K}.${Path<T[K]>}`
: K
: never;
type UserPath = Path<AppConfig>;
// "database" | "database.host" | "database.port"
// | "database.credentials" | "database.credentials.username"
// | "database.credentials.password" | "logging" | "logging.level" | "logging.file"
这个类型的递归思路:如果当前键对应的值还是对象,就在键名后面拼接 . 再递归进去。
递归深度限制
TypeScript 编译器对递归类型的展开有深度限制——默认约 50 层。这不是 bug,而是为了防无限递归把编译器卡死。如果你写的递归类型超了这个深度,TS 会报错 Type instantiation is excessively deep。
常见解决方案:
- 检查递归终止条件是否真的能触发(比如是否忽略了原始类型的判断)
- 减少不必要的嵌套层级
- 用尾递归风格(把累积结果作为额外类型参数传递)来优化
小结
- 递归类型在定义中引用自身,表达嵌套结构
- JSON 类型是最经典的递归类型定义
DeepPartial和DeepReadonly依赖递归做深度转换- 递归深度默认 ~50 层,超出会报
excessively deep - 递归类型 + 条件类型 + 映射类型是 TypeScript 类型体操的三板斧