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

TypeScript含泛型双向递归类型<A↔B>的类型推断问题及疑问

问题描述

我写了下面这段TypeScript代码,现在碰到俩问题:

  1. Audit和User全被推断成any类型,没法实现Audit.preds.user().preds.audits()...这种深层递归访问,实际场景比这个复杂多了。
  2. 把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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 04:20:34