You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决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 parent circularly 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 01:31:01