解决TypeScript中点表示法对象路径递归类型过深问题
解决TypeScript递归对象路径类型的无限深度报错问题
我正在实现用点表示法(如users.1.name)表示对象路径的TypeScript类型定义,当前方案在普通对象上可以正常工作,但处理递归类型时会触发错误:
Type instantiation is excessively deep and possibly infinite.
以下是原实现代码:
type PathImpl<T, K extends keyof T> = K extends (string | number) ? T[K] extends Record<string, any> ? T[K] extends ArrayLike<any> ? K | `${K}.${PathImpl<T[K], number>}` : K | `${K}.${PathImpl<T[K], keyof T[K]>}` : K : never; export type ObjectPath<T> = PathImpl<T, keyof T> | keyof T; // 普通对象测试正常 type User = { name: string, address: { lines: { street: string, streetNumber: number }[], zip: string, } }; const streetNumber: ObjectPath<User> = "address.lines.2.streetNumber" // ✅ const foo: ObjectPath<User> = "address.lines.2.foo" // ❌ 正确拒绝无效路径 // 递归类型触发报错 type UserWithFriends = { name: string, friends: UserWithFriends[] } // TS Error: Type instantiation is excessively deep and possibly infinite. const f: ObjectPath<UserWithFriends> = 'name'
问题原因
原类型PathImpl是无限制递归的,当遇到UserWithFriends这种自引用的递归结构时,TypeScript编译器会不断尝试展开嵌套的类型,超出了内置的递归深度限制,从而抛出错误。
解决方案1:限制递归深度
通过引入一个深度参数,当递归达到指定层数后停止展开,避免无限递归。
// 可自定义最大递归深度,默认10层 type PathImpl<T, K extends keyof T, Depth extends number = 10, CurrentDepth extends number[] = []> = // 达到最大深度时停止递归 CurrentDepth['length'] extends Depth ? K : K extends (string | number) ? T[K] extends Record<string, any> ? T[K] extends ArrayLike<any> ? K | `${K}.${PathImpl<T[K], number, Depth, [...CurrentDepth, 0]>}` : K | `${K}.${PathImpl<T[K], keyof T[K], Depth, [...CurrentDepth, 0]>}` : K : never; export type ObjectPath<T, Depth extends number = 10> = PathImpl<T, keyof T, Depth> | keyof T; // 测试递归类型 type UserWithFriends = { name: string, friends: UserWithFriends[] } const f: ObjectPath<UserWithFriends> = 'name' // ✅ const friendName: ObjectPath<UserWithFriends> = 'friends.0.name' // ✅ // 支持最多10层嵌套路径,超过则停止展开
优点:可控性强,明确知道最大支持的路径层数,适合对深度有预期的业务场景;缺点:极端超深度场景下可能无法覆盖,但日常开发足够使用。
解决方案2:自动检测递归引用
利用TypeScript条件类型的特性,当检测到类型递归引用自身时停止展开,避免无限递归。
type PathImpl<T, K extends keyof T> = K extends (string | number) ? T[K] extends Record<string, any> ? // 检测是否递归引用自身,是则停止递归 T[K] extends T ? K : T[K] extends ArrayLike<any> ? K | `${K}.${PathImpl<T[K], number>}` : K | `${K}.${PathImpl<T[K], keyof T[K]>}` : K : never; export type ObjectPath<T> = PathImpl<T, keyof T> | keyof T; // 测试递归类型 type UserWithFriends = { name: string, friends: UserWithFriends[] } const f: ObjectPath<UserWithFriends> = 'name' // ✅ const friendName: ObjectPath<UserWithFriends> = 'friends.0.name' // ✅ const deepFriend: ObjectPath<UserWithFriends> = 'friends.0.friends.0.name' // ✅
优点:更灵活,无需手动设置深度,能自动处理合理的递归嵌套;缺点:对于极端复杂的递归结构,仍可能触发编译器深度限制,但足以应对绝大多数日常开发场景。
内容的提问来源于stack exchange,提问作者Tom Esterez
相关产品推荐
相关产品推荐

