首页 / TypeScript 入门教程 / 递归类型

TypeScript 入门教程

递归类型

本教程共 80 篇 · 第 50 篇 · 更新于 2026-08-10 · 约 10 分钟阅读

TypeScriptTypeScript 入门教程递归类型Recursive TypesDeepPartialJSON

本节目标:理解递归类型的自引用机制,学会用它表达树形结构、JSON 数据,并写出 DeepPartial、DeepReadonly 等深度工具类型。

递归类型是什么

递归类型就是在类型定义里引用自己的类型。它的典型场景是描述嵌套结构——文件夹套文件夹、列表套列表。

interface TreeNode {
  value: string;
  children?: TreeNode[]; // 引用自身
}

TreeNodechildren 属性类型是 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 的逻辑:

  1. T extends object:如果是对象,就遍历它的每个键
  2. [P in keyof T]?:每个属性变成可选
  3. DeepPartial<T[P]>:对属性值类型递归调用 DeepPartial
  4. 如果是原始类型(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 类型是最经典的递归类型定义
  • DeepPartialDeepReadonly 依赖递归做深度转换
  • 递归深度默认 ~50 层,超出会报 excessively deep
  • 递归类型 + 条件类型 + 映射类型是 TypeScript 类型体操的三板斧