TypeScript工厂函数:键数组动态返回类型与无限递归错误排查
场景与需求
我在AWS Lambda函数中使用UserModule、NotificationsModule、CompanyModule等模块,定义了如下Modules接口:
interface Modules { company: ICompanyModule user: { base: IUserModule } notifications: { settings: INotificationSettingsModule details: INotificationDetailsModule } }
为减少手动导入和实例化的工作量,我编写了factory方法,期望根据传入的Modules接口键(支持嵌套路径,如user.base)返回仅包含所需模块的对象,示例如下:
// 单个模块 const module = factory('company') // 类型应为ICompanyModule // 多个模块 const modules = factory(['company', 'user.base']) // 类型应为{ company: ICompanyModule, 'user.base': IUserModule }
问题描述
我编写了Path、PathValue、ReturnObject等类型定义,调用factory时能得到正确的对象结构,但TypeScript在ReturnObject的{ [Key in P[number]]: ... }处抛出Type instantiation is excessively deep and possibly infinite错误。推测是P可扩展为Array<Path<T>>导致递归过深,求解决该错误的方法。
附当前类型定义代码:
// 获取Modules接口的所有键及嵌套键 type Path<T, Key extends keyof T = keyof T> = Key extends string ? T[Key] extends Record<string, unknown> ? | `${Key}.${Path<T[Key], Exclude<keyof T[Key], keyof Array<unknown>>> & string}` | `${Key}.${Exclude<keyof T[Key], keyof Array<unknown>> & string}` | Key : Key : never // 获取Path对应的类型值 type PathValue<T, P extends Path<T>> = P extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? Rest extends Path<T[Key]> ? PathValue<T[Key], Rest> : never : never : P extends keyof T ? T[P] : never // 根据单个键或键数组构建返回对象类型 type ReturnObject<T extends Object, P extends Path<T> | Array<Path<T>>> = P extends Array<Path<T>> ? { [Key in P[number]]: Key extends keyof T ? T[Key] extends Object ? Key extends Path<T[Key]> ? ReturnObject<T[Key], Key> : T[Key] : never : Key extends `${infer K}.${infer Rest}` ? K extends keyof T ? T[K] extends Object ? Rest extends Path<T[K]> ? ReturnObject<T[K], Rest> : never : never : never : never } : P extends keyof T ? T[P] : P extends `${infer K}.${infer Rest}` ? K extends keyof T ? T[K] extends Object ? Rest extends Path<T[K]> ? ReturnObject<T[K], Rest> : never : T[K] : never : never // factory函数声明 declare function factory<T extends ModuleFactory, P extends Path<T> | Array<Path<T>>>( modules: P ): ReturnObject<T, P>
解决方案
核心优化:复用PathValue避免重复递归
ReturnObject的问题在于处理数组路径时,重复调用自身递归解析路径,导致TypeScript类型计算栈溢出。可以直接复用已实现的PathValue类型,简化ReturnObject的逻辑,彻底消除不必要的递归嵌套:
修改后的ReturnObject类型:
type ReturnObject<T extends object, P extends Path<T> | Array<Path<T>>> = P extends Array<Path<T>> ? { [K in P[number]]: PathValue<T, K> } : PathValue<T, P>;
优化说明
- 当传入路径数组时,直接遍历数组中的每个路径,通过
PathValue获取对应的类型,不再触发ReturnObject自身的递归调用,大幅降低类型计算的深度。 - 单个路径的情况直接复用
PathValue的结果,逻辑更简洁,同时保证类型正确性。
额外优化:简化Path类型(可选)
原Path类型生成了冗余的路径组合,进一步增加了类型计算压力。可以简化为以下版本,只生成必要的路径:
type Path<T> = T extends object ? { [K in keyof T & string]: | K | `${K}.${Path<T[K]>}` }[keyof T & string] : never;
这个版本的Path会递归生成所有层级的路径(如company、user、user.base等),避免原版本中重复的中间路径定义,进一步提升类型计算效率。
验证效果
修改后,调用示例的类型推导完全符合预期:
factory('company')返回类型为ICompanyModulefactory(['company', 'user.base'])返回类型为{ company: ICompanyModule, 'user.base': IUserModule }
同时TypeScript不再抛出递归过深的错误。
内容的提问来源于stack exchange,提问作者Ben Young

