导读:本期聚焦于小伙伴创作的《如何在TypeScript类型体操中将对象所有属性类型路径映射到Promise.all的返回类型?》,敬请观看详情。对象属性路径提取是TypeScript高级类型里的典型难点,不少人在写工具类型时卡在如何把嵌套对象的每一条键路径对应的属性类型,转成Promise.all所能接受的元组并返回结构一致的结果。本文从Paths工具类型切入,先递归算出对象全部叶子路径,再用路径索引取出原类型,构造元组交给Promise.all推断。重点说明递归条件中避免无限循环的办法,以及遇到联合类型路径时如何用分布式条件类型逐个映射。掌握这套写法后,你可以把任意配置对象、API返回结构批量转成并发请求类型,既保留原形状又获得异步并行能力。

在TypeScript类型编程里,我们经常需要把一个对象类型的所有属性访问路径找出来,并将这些路径对应的具体类型组成一个元组,再借助Promise.all得到并行执行后的结果类型。这种需求在封装并发请求工具、批量调度接口时非常实用。下面直接展开实现思路与完整代码。

如何在TypeScript类型体操中将对象所有属性类型路径映射到Promise.all的返回类型?

一、理解对象属性类型路径

所谓“属性类型路径”,是指从对象根出发,通过一连串键访问最终到达某个叶子属性所经过的键名序列。例如对象{ a: { b: number }, c: string },它的路径包括'a''a.b''c'。在类型层面,我们需要用模板字面量类型把嵌套键拼接成字符串联合。

为什么不直接用原对象属性类型?因为Promise.all接收的是值元组,而不是对象。只有把每条路径对应的“值类型”提取出来排成数组,才能在运行时用obj[keyPath]取值并并发执行,类型上也要同步映射才能保证安全。

二、递归提取所有路径的工具类型

我们先写一个Paths<T>类型,递归遍历对象,遇到非对象类型就停止并输出当前路径,遇到对象就继续往下拼键名。需要注意限制递归深度,防止循环引用导致编译器爆栈。

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

type Paths<T, D extends number = 5> = [D] extends [never]
  ? never
  : T extends Primitive
  ? ''
  : {
      [K in keyof T & string]:
        | K
        | (Paths<T[K], Prev[D]> extends infer P extends string
            ? P extends ''
              ? never
              : `${K}.${P}`
            : never);
    }[keyof T & string];

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8];

上面代码中,D是深度计数器,每递归一层就减一,到never就停止。对每一层键K,要么返回自身路径,要么拼接子路径。使用Prev[D]拿到上一层的剩余深度,从而安全控制层数。

该写法的优点是能处理任意扁平或嵌套结构,并且通过深度参数避免无限递归。缺点是路径以字符串形式存在,后续索引时需要再写一层映射。

三、根据路径取类型并构造元组

有了路径联合后,我们用条件类型把单个路径转成对应属性类型。由于路径是联合类型,分布式条件类型会逐个处理,最终得到所有属性类型的联合,但我们真正要的是“按路径顺序组成的元组”,因此需结合Promise.all的推导。

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

declare function getByPath<T, P extends string>(obj: T, path: P): PropByPath<T, P>;

async function mapPathsToPromiseAll<T, P extends Paths<T>>(
  obj: T,
  paths: P[]
): Promise<{ [I in keyof P]: PropByPath<T, P[I] & string> }> {
  const promises = paths.map(p => Promise.resolve(getByPath(obj, p)));
  return Promise.all(promises) as any;
}

PropByPath按点号拆分路径并逐级索引,若路径非法则返回never。函数mapPathsToPromiseAll接收对象与路径数组,把每个路径取出的值包成Promise,交给Promise.all。返回类型用映射类型保留元组位置,从而实现“原对象形状的类型路径”到“并发结果元组”的映射。

这种结构让你在调用时既能享受Promise.all的并行能力,又能在类型上明确知道第几个结果对应哪条路径的原始类型,避免手动any断言带来的隐患。

四、完整示例与注意事项

下面给出一个可运行示例,展示如何对配置对象并发读取字段并拿到精确类型。

const config = {
  db: { host: '127.0.0.1', port: 5432 },
  api: { timeout: 3000 }
};

type Cfg = typeof config;
type CfgPaths = Paths<Cfg>;

async function main() {
  const paths: CfgPaths[] = ['db.host', 'db.port', 'api.timeout'];
  const res = await mapPathsToPromiseAll(config, paths);
  // res 类型为 [string, number, number]
  const [host, port, timeout] = res;
  console.log(host, port, timeout);
}

示例中res的类型被推断为[string, number, number],完全对应路径顺序。注意如果对象包含可选属性,PropByPath应额外处理undefined,否则索引可能报错;可在映射时加上| undefined兼容。

当路径来自外部输入而非字面量时,TypeScript无法缩小具体字符串,此时Promise.all返回类型会退化为联合数组。建议在内部工具中约束路径参数为Paths<T>字面量联合,即可在大部分场景保持类型精确。

TypeScript类型体操Promise_all修改时间:2026-08-11 17:42:31

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