如何解决TypeScript中TS2615循环引用与点属性路径问题?
问题:TypeScript循环引用导致的映射类型错误
背景
我尝试实现类型安全的点属性路径方案,基于相关思路修改了类型声明,用于生成嵌套对象的点符号路径字符串类型。
核心类型声明
type PathsToStringProps<T> = T extends string ? [] : { [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>] }[Extract<keyof T, string>]; type Join<T extends any[], D extends string> = T extends [] ? never : T extends [infer F] ? F : T extends [infer F, ...infer R] ? F extends string ? `${F}${D}${Join<Extract<R, string[]>, D>}` : never : string; type DottedLanguageObjectStringPaths<T extends object> = Join<PathsToStringProps<T>, ".">;
业务类型定义
基于上述类型,我定义了如下业务类型:
interface ITest { a: string; } interface IOptionBase<T extends object> { name: string; type: number; getFunc?: () => T; prop: DottedLanguageObjectStringPaths<T>; } interface IOption1<T extends object> extends IOptionBase<T> { type: 1; } interface IOption2<T extends object> extends IOptionBase<T> { type: 2; } type ActualOption<T extends object> = IOption1<T> | IOption2<T>; type Wrapper<T extends object> = ActualOption<T> & ITest; const x: Wrapper<ITest> = { type: 2, name: 'test', prop: 'a' };
这段代码可以正常编译运行。
问题出现
当给ITest添加自引用属性后:
interface ITest { a: string; parent?: ITest; }
赋值给x的字面量出现编译器错误:
TS2615: Type of property
parentcircularly references itself in mapped type
奇怪的是,如果自引用是数组形式则不会报错:
interface ITest { a: string; parent?: ITest[]; }
需要解决这个循环引用导致的类型错误,同时保留包装结构和父引用的设计。
原因分析
TypeScript处理递归映射类型时,直接的对象自引用会让编译器陷入无限递归计算:PathsToStringProps会不断展开parent对应的ITest类型,无法终止计算,最终抛出循环引用错误。而数组形式的自引用不会触发问题,因为数组的keyof是数字索引,Extract<keyof T, string>会过滤掉这些索引,PathsToStringProps<ITest[]>会直接终止递归。
解决方案
方案1:限制递归深度
通过添加深度参数,达到指定深度后停止类型展开,避免无限递归:
type PathsToStringProps<T, Depth extends number = 10> = Depth extends 0 ? never : // 达到最大深度时终止递归 T extends string ? [] : { [K in Extract<keyof T, string>]: T[K] extends object ? [K, ...PathsToStringProps<T[K], Depth extends number ? Depth - 1 : never>] : [K] }[Extract<keyof T, string>]; type Join<T extends any[], D extends string> = T extends [] ? never : T extends [infer F] ? F : T extends [infer F, ...infer R] ? F extends string ? `${F}${D}${Join<Extract<R, string[]>, D>}` : never : string; type DottedLanguageObjectStringPaths<T extends object, Depth extends number = 10> = Join<PathsToStringProps<T, Depth>, ".">;
使用修改后的类型,自引用场景可正常编译,还支持指定深度的嵌套路径:
interface ITest { a: string; parent?: ITest; } const x: Wrapper<ITest> = { type: 2, name: 'test', prop: 'a' }; // 支持嵌套路径,如'parent.a'、'parent.parent.a'(最多到指定深度) const y: Wrapper<ITest> = { type: 1, name: 'test2', prop: 'parent.a' };
方案2:跳过循环引用类型
如果不需要深层自引用路径,可以在类型判断中直接跳过与原类型相同的引用:
type PathsToStringProps<T, Original = T> = T extends string ? [] : T extends Original ? never : // 遇到原类型时终止递归 { [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K], Original>] }[Extract<keyof T, string>]; type Join<T extends any[], D extends string> = T extends [] ? never : T extends [infer F] ? F : T extends [infer F, ...infer R] ? F extends string ? `${F}${D}${Join<Extract<R, string[]>, D>}` : never : string; type DottedLanguageObjectStringPaths<T extends object> = Join<PathsToStringProps<T>, ".">;
这种方案下,prop只能取非自引用路径(如'a'),无法获取'parent.a'这类嵌套路径,适合不需要深层自引用的场景。
内容的提问来源于stack exchange,提问作者F-H
相关产品推荐
相关产品推荐

