TypeScript含泛型双向递归类型<A↔B>的类型推断问题及疑问
问题描述
我写了下面这段TypeScript代码,现在碰到俩问题:
Audit和User全被推断成any类型,没法实现Audit.preds.user().preds.audits()...这种深层递归访问,实际场景比这个复杂多了。- 把
DormEdge泛型的默认值= false删掉之后,功能就正常了,想搞明白为啥会这样。
使用版本:typescript@5.3.3
type DormNodePredicate = DormEdge | (() => DormNode<DormNodePredicateRecord>); interface DormNodePredicateRecord { [key: string]: DormNodePredicate; } export class DormNode<DNPR extends DormNodePredicateRecord> { constructor(public preds: DNPR) {} } enum PredicateType { STRING = "string" } export class DormEdge<AsArray extends boolean = false> { constructor(private predType: PredicateType, asArray: AsArray = false as AsArray) {} } const Audit = new DormNode({ user: () => User }) const User = new DormNode({ name: new DormEdge(PredicateType.STRING), audits: () => Audit });
解决方案
想搞定类型推断的问题,关键是让TypeScript能正确追踪递归节点的类型,得调整类型定义和节点的声明方式:
1. 调整类型系统,支持递归推断
先重新定义相关类型,让函数返回的节点类型能被TypeScript正确捕获:
// 先做前向类型引用 type DormNode<T extends DormNodePredicateRecord> = InstanceType<typeof DormNodeClass<T>>; interface DormNodePredicateRecord { [key: string]: DormEdge | (() => DormNode<any>); } class DormNodeClass<T extends DormNodePredicateRecord> { constructor(public preds: T) {} } enum PredicateType { STRING = "string" } // 要么移除泛型默认值,要么用的时候显式传参数 export class DormEdge<AsArray extends boolean> { constructor(private predType: PredicateType, public asArray: AsArray) {} } // 用类型断言或者分步声明解决循环引用的推断问题 const Audit: DormNode<{ user: () => DormNode<typeof User["preds"]> }> = new DormNodeClass({ user: () => User }) as any; const User = new DormNodeClass({ name: new DormEdge(PredicateType.STRING, false), audits: () => Audit });
或者利用TypeScript 5.0+的递归类型改进,更简洁地调整:
interface DormNodePredicateRecord { [key: string]: DormEdge | (() => DormNode<DormNodePredicateRecord>); } export class DormNode<DNPR extends DormNodePredicateRecord> { constructor(public preds: DNPR) {} } enum PredicateType { STRING = "string" } // 显式指定泛型参数,避免默认值带来的推断模糊 export class DormEdge<AsArray extends boolean> { constructor(private predType: PredicateType, public asArray: AsArray) {} } // 用typeof提前引用,配合类型断言解决循环推断的问题 const User: DormNode<{ name: DormEdge<false>; audits: () => DormNode<typeof Audit["preds"]>; }> = new DormNode({ name: new DormEdge(PredicateType.STRING, false), audits: () => Audit }) as any; const Audit = new DormNode({ user: () => User });
2. 实现深层递归访问的类型安全
调整之后,TypeScript就能正确推断Audit.preds.user().preds.audits()这类递归访问的类型,不会再出现any的情况。
底层原因解释
当给DormEdge加泛型默认值= false时,TypeScript的类型推断会陷入循环推断的死局:
- 声明
Audit的时候,它的preds.user返回User,但User还没定义,TypeScript尝试推断User的类型,发现User的preds.name是DormEdge<false>(因为用了默认值)。 - 但
Audit和User互相引用,TypeScript没法完成完整的类型推断,只能退而求其次用any兜底。
删掉泛型默认值之后,你必须显式传AsArray的参数(比如false),这会强制TypeScript立刻确定DormEdge的具体类型,打破了循环推断的模糊性,让TypeScript能正确追踪Audit和User之间的递归类型关系。
说白了就是:泛型默认值让TypeScript处理循环引用时,没法确定具体的泛型参数,只能用any;而显式传参消除了这种不确定性,类型推断就能正常走下去了。
内容的提问来源于stack exchange,提问作者Viany Manuel
相关产品推荐
相关产品推荐

