实现深度对象路径函数参数的智能提示与类型检查,规避递归类型限制
解决TypeScript嵌套对象路径访问函数的类型递归深度问题
问题背景
假设有如下嵌套对象:
const obj = { something: 123, otherThing: "asd", nested: { nestedSomething: 456, nestedOther: "fgh", deepNested: { deepNested1: "hello", deepNested2: 42, deepNestedArr: ["a", "b", "c"], }, }, };
需要实现一个access函数,支持按路径访问对象值,示例如下:
access(obj, "something") // 返回number类型 access(obj, "nested", "nestedOther") // 返回string类型 access(obj, "nested", "deepNested", "deepNestedArr") // 返回string[]类型 access(obj, "nested", "deepNested", "deepNestedArr", 0) // 返回string类型
原实现的问题
最初实现了两个工具类型:
AllPaths:生成对象所有叶子节点的路径联合类型
type AllPaths<Obj extends object, Key = keyof Obj> = Key extends keyof Obj ? Readonly<Obj[Key]> extends Readonly<Array<any>> ? [Key] | [Key, number] : Obj[Key] extends object ? [Key] | [Key, ...AllPaths<Obj[Key]>] : [Key] : never;
GetTypeFromPath:根据对象类型和路径返回对应值的类型
type GetTypeFromPath<Obj extends object, Path extends PropertyKey[]> = Path extends [ infer Head, ...infer Tail extends PropertyKey[] ] ? Head extends keyof Obj ? Obj[Head] extends object ? GetTypeFromPath<Obj[Head], Tail> : Obj[Head] : never : Obj;
但将它们用于泛型函数时,TypeScript服务会出现卡顿,并抛出「类型实例化过深」或「比较类型时栈深度过大」的错误:
declare function access< Obj extends object, Paths extends AllPaths<Obj> = AllPaths<Obj>, Ret = GetTypeFromPath<Obj, Paths> >( obj: Obj, ...path: Paths ): Ret; const res = access(obj, "nested", "deepNested"); // 类型计算失败
可行实现方案
方案1:函数重载(推荐,无递归深度问题)
通过编写多层函数重载,覆盖常用的嵌套深度(比如3-5层),既满足API边界的需求,又避免递归类型的性能问题:
// 基础情况:无路径参数,返回原对象 declare function access<T>(obj: T): T; // 1层路径 declare function access<T, K1 extends keyof T>(obj: T, key1: K1): T[K1]; // 2层路径 declare function access<T, K1 extends keyof T, K2 extends keyof T[K1]>( obj: T, key1: K1, key2: K2 ): T[K1][K2]; // 3层路径(支持数组索引) declare function access<T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2] | number>( obj: T, key1: K1, key2: K2, key3: K3 ): K3 extends number ? T[K1][K2] extends readonly (infer U)[] ? U : never : T[K1][K2][K3]; // 4层路径(支持数组索引) declare function access<T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2] | number, K4 extends keyof ( K3 extends number ? (T[K1][K2] extends readonly (infer U)[] ? U : never) : T[K1][K2][K3] ) | number>( obj: T, key1: K1, key2: K2, key3: K3, key4: K4 ): K4 extends number ? (K3 extends number ? T[K1][K2] extends readonly (infer U)[] ? U extends readonly (infer V)[] ? V : never : never : T[K1][K2][K3] extends readonly (infer U)[] ? U : never ) : K3 extends number ? (T[K1][K2] extends readonly (infer U)[] ? U[K4] : never) : T[K1][K2][K3][K4];
优势:
- 无递归类型,TypeScript服务不会卡顿
- 提供精确的智能提示和错误检查
- 支持数组索引访问
方案2:限制递归深度的泛型类型
如果需要支持更深的嵌套,可以通过限制递归深度(比如设置最大深度为5)来避免触发TypeScript的递归限制:
// 设置最大递归深度 type MaxDepth = 5; // 生成路径类型,限制深度 type PathImpl<T, Depth extends number[] = []> = Depth['length'] extends MaxDepth ? never : T extends readonly any[] ? [number] | [number, ...PathImpl<T[number], [...Depth, 0]>] : T extends object ? keyof T extends infer K extends keyof T ? [K] | [K, ...PathImpl<T[K], [...Depth, 0]>] : never : never; type Path<T> = PathImpl<T>; // 根据路径获取值类型,限制深度 type GetValueAtPath<T, P extends PropertyKey[], Depth extends number[] = []> = Depth['length'] extends MaxDepth ? T : P extends [infer First extends keyof T, ...infer Rest extends PropertyKey[]] ? GetValueAtPath<T[First], Rest, [...Depth, 0]> : T extends readonly (infer U)[] ? U : T; declare function access<Obj extends object, P extends Path<Obj>>( obj: Obj, ...path: P ): GetValueAtPath<Obj, P>;
优势:
- 支持更深的嵌套(可调整MaxDepth值)
- 保持参数列表的灵活性
- 同样提供智能提示和错误检查
验证效果
使用方案1或方案2后,以下调用都能正常推断类型并检查错误:
const res1 = access(obj, "something"); // number const res2 = access(obj, "nested", "nestedOther"); // string const res3 = access(obj, "nested", "deepNested", "deepNestedArr"); // string[] const res4 = access(obj, "nested", "deepNested", "deepNestedArr", 0); // string const res5 = access(obj, "invalidPath"); // 类型错误,提示无效路径
内容的提问来源于stack exchange,提问作者RestOfTheBothWorlds
相关产品推荐
相关产品推荐

