在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