← 返回题目列表

TypeScript 递归类型适合什么场景?为什么会遇到深度限制?

困难 第 27 / 32 题 更新于 2026/07/29
TypeScript递归类型类型编程

简化版

递归类型是在类型定义里引用自身,适合描述树、JSON、菜单、路由、嵌套数组、深度只读等嵌套结构。

它也常用于类型工具,比如 DeepPartialDeepReadonly。但递归类型过深或分支过多会让编译器实例化成本变高,甚至报“类型实例化过深”的错误。

详细版

type TreeNode = {
  id: string;
  children?: TreeNode[];
};

深度工具类型:

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

这种写法能处理嵌套对象,但遇到函数、数组、Date、Map 等对象时需要额外分支。复杂递归类型要控制深度和范围,不要为了类型完美拖慢整个项目。

完整版教学

一、递归类型描述嵌套结构

最自然的场景是树。

type MenuItem = {
  label: string;
  path: string;
  children?: MenuItem[];
};

一个菜单项的 children 仍然是菜单项数组,这就是递归。

递归类型不是高级题专属,树形 UI、路由和评论楼中楼都在用它。

二、JSON 类型也是递归结构

type Json =
  | string
  | number
  | boolean
  | null
  | Json[]
  | { [key: string]: Json };

这能表达 JSON 可以嵌套数组和对象。

JSON 值是否符合
"a"
[1, {"x": true}]
() => 1

三、递归工具类型的基本套路

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

这个类型会把对象所有层级都变成可选。

type Config = {
  db: { host: string; port: number };
};

type PartialConfig = DeepPartial<Config>;

四、object 分支不等于普通对象

object 包含数组、函数、Date、Map 等非原始值。

type Bad<T> = T extends object ? DeepPartial<T> : T;

如果不做特殊处理,数组和函数可能被错误展开。

实际工具类型通常会先处理函数、数组、内置对象,再递归普通对象。

五、为什么会有深度限制

编译器需要不断实例化类型。

Deep<A>
  -> Deep<B>
    -> Deep<C>
      -> ...

如果递归没有出口,或联合分支不断膨胀,类型检查成本会快速上升。50 层嵌套对象再叠加多个联合分支,可能让编辑器明显变慢。

六、如何控制复杂度

常见做法:

  • 明确递归出口
  • 限制递归层数
  • 避免大联合分发
  • 对数组、函数、Date 做特殊分支
  • 不追求所有运行时形态都由类型精确表达
type Depth = 0 | 1 | 2 | 3;

复杂类型工具可以引入深度参数,但这也会增加阅读成本。

七、常见误区与追问

  • 误区:递归类型只用于类型体操。 树、菜单、路由、JSON 都是实际场景。
  • 误区:T extends object 就只匹配普通对象。 数组、函数和 Date 也会被包含。
  • 误区:递归类型越精确越好。 太复杂会拖慢编译和编辑器提示。
  • 误区:递归不需要出口。 没有清晰出口会导致实例化过深。
  • 追问:DeepPartial 有哪些坑? 数组、函数、内置对象和联合类型都要额外处理。
  • 追问:类型深度报错怎么处理? 简化类型、加出口、限制深度或拆分工具类型。

八、加强记忆

记忆方式:

nested data -> recursive type
deep utility -> recursive mapped type
too deep     -> compiler cost

回答时先给树和 JSON 两个例子,再讲 DeepPartial,最后提醒 object 分支和深度限制,是比较完整的答法。