导读:本期聚焦于大卫创作的《如何用TypeScript类型体操把对象的所有属性路径映射为联合类型?》,敬请观看详情。想知道Vue.use那类框架是如何实现a.b.c形式路径的完整类型提示的吗?这篇文章手把手带你实现一个深度递归的工具类型,把嵌套对象中每一条属性访问路径都提取成字符串字面量,最终汇总成一个联合类型。内容涵盖递归类型的基本套路、处理数组和元组的技巧、如何避免循环引用导致的类型爆炸,以及在真实项目里用这些路径联合类型约束参数、实现类型安全的表单取值函数等实践方案,配合可运行的代码示例,帮你彻底搞懂路径类型映射的底层逻辑。

写前端代码时经常碰到一个需求:给一个函数传入字符串路径来读取深层属性,比如getPath(obj, "user.address.city")。如果路径参数是普通的string类型,拼错一个字母TypeScript也不会报错。能不能让TypeScript自动推导出这个对象里所有合法的路径,拼成"user" | "user.address" | "user.address.city"这样的联合类型呢?这就是类型体操里非常经典的"属性路径映射到联合类型"问题,下面我们一步步把它实现出来。

如何用TypeScript类型体操把对象的所有属性路径映射为联合类型?

从最简单的一层属性开始

先把问题缩小:只处理一层属性。给定类型{ a: number; b: string },我们想得到"a" | "b"。这可以用keyof直接做到,keyof T返回的就是对象所有键组成的联合类型。但要注意边界情况:如果传入的对象上有方法或者数组,keyof会把它们也算进去,而方法通常不参与路径遍历。所以第一步的实现通常长这样:

type FirstLevel<T> = {
  [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];

type A = FirstLevel<{ a: number; run: () => void }>;
// 结果:"a"

这段代码用了两个技巧:一是映射类型[K in keyof T]把每个键遍历一遍,二是最后的[keyof T]做索引访问,把映射结果拍平成联合类型。函数属性被映射成never后,在联合类型中会被自动吞掉,这是处理联合类型时非常好用的特性。理解了这个骨架,后面的递归只是在它上面加一层"深入"的逻辑。

递归展开嵌套路径,用模板字面量拼接

真正的难点在于嵌套对象。路径要一层层往下钻,每钻一层就把当前键名拼到路径前面,这就要请出模板字面量类型。判断"值是不是一个应该继续遍历的对象",可以用T[K] extends object做粗略判断,再排除掉函数和原始类型。完整实现如下:

type Primitive = string | number | boolean | bigint | symbol | null | undefined;

// 判断是否为值得继续深入的对象
type IsPlainObject<T> = T extends Primitive ? false
  : T extends Array<any> ? false
  : T extends Function ? false
  : true;

type PathOf<T> = {
  [K in keyof T & string]: IsPlainObject<T[K]> extends true
    ? K | `${K}.${PathOf<T[K]>}`
    : K;
}[keyof T & string];

type User = {
  id: number;
  name: { first: string; last: string };
  tags: string[];
};

type Result = PathOf<User>;
// "id" | "name" | "name.first" | "name.last" | "tags"

这里有几个细节值得展开。第一,keyof T & string是为了过滤掉symbol类型的键,因为模板字面量只能拼接字符串。第二,递归分支里返回的是K | \`${K}.${PathOf<T[K]>}\`,也就是说父路径本身也在联合类型里,这样既允许getPath(user, "name")也允许getPath(user, "name.first")。第三,TypeScript 4.1之后才支持模板字面量类型和这种递归,旧版本会直接报错,这一点在团队协作时要注意tsconfig的版本要求。

数组的处理有两种策略。上面的实现把数组当叶子节点,路径停在"tags"。如果想让数组元素也参与路径,可以额外处理:对readonly数组用number做索引,让路径变成"tags.0"这种带索引的形式,代码如下:

type PathOfDeep<T> = {
  [K in keyof T & string]: T[K] extends readonly (infer E)[]
    ? K | `${K}.${number}` | `${K}.${number}.${PathOfDeep<E>}`
    : IsPlainObject<T[K]> extends true
      ? K | `${K}.${PathOfDeep<T[K]>}`
      : K;
}[keyof T & string];

注意这里用的是infer E来提取数组元素类型再递归,而不是直接对数组本身递归,否则会陷入无限展开。索引位置用number而不是具体数字,是因为我们无法在类型层面枚举数组长度,只约束"这里应该是一个数字下标"即可,运行时传入"tags.0"这样的字符串是能匹配上的。

循环引用与类型爆炸的防范

递归类型最大的坑是循环引用。比如type Node = { value: number; next: Node },如果直接递归展开,理论上路径有无穷多条,TypeScript会报"类型实例化过深"的错误。虽然新版编译器对循环类型有一定检测能力(遇到已经展开过的类型会标记为循环并终止),但保险起见,实际工程中最好加上深度限制或者显式的终止标记。

一种常见做法是引入一个层数计数器,递归到指定深度就停止:

type PathLimited<T, D extends number = 5> = D extends 0
  ? never
  : {
      [K in keyof T & string]: IsPlainObject<T[K]> extends true
        ? K | `${K}.${PathLimited<T[K], D extends 1 ? 0 : Dec<D>>}`
        : K;
    }[keyof T & string];

// 简单的减一实现
type Dec<N extends number> = [never, 0, 1, 2, 3, 4][N];

另一个坑是对象特别大时,路径联合类型的数量会呈指数增长。一个有20个字段、每个字段又嵌套几层的接口,展开后的联合类型可能有几百个成员,编辑器提示会明显变慢。经验值是:当联合成员超过几百个时,考虑用分层的参数签名替代,比如把路径改成元组形式["name", "first"],用as const推导,类型计算量会小很多,而且还能避免字符串拼接的类型开销。

落地应用:类型安全的取值函数

有了路径联合类型,就可以封装一个运行时安全、编译期严格的getPath。函数签名用泛型约束路径参数,再用一个辅助类型根据路径反查出值的类型:

type PathValue<T, P extends string> =
  P extends `${infer Head}.${infer Rest}`
    ? Head extends keyof T
      ? PathValue<T[Head], Rest>
      : never
    : P extends keyof T
      ? T[P]
      : never;

function getPath<T, P extends PathOf<T>>(obj: T, path: P): PathValue<T, P> {
  return path.split(".").reduce<any>((cur, key) => cur?.[key], obj);
}

const city = getPath(user, "name.first"); // 类型自动推导为 string
// getPath(user, "name.middle"); // 编译报错,路径不存在

PathValue的实现同样用到了infer模式匹配:先把路径按第一个点拆成头部和剩余部分,头部去对象里索引,剩余部分继续递归。这样写出来的getPath,传路径时有自动补全,写错了立刻红线提示,返回值类型还精确到具体的属性类型,体验和直接写user.name.first几乎没有差别。

这套思路的适用场景远不止取值函数。表单校验库可以用路径联合类型约束错误信息里的字段名,表格组件可以用它约束列的字段取值,状态管理库可以用它约束watch的监听目标。核心套路总结起来就三板斧:keyof遍历键、模板字面量拼路径、infer做模式匹配与反查。掌握之后,大部分路径相关的类型体操题目都能套这个模板解决。

TypeScript类型体操联合类型修改时间:2026-09-06 20:44:37

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260906/51785.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。