如何关联由通用 属性 链接的不同参数

How to relate different parameters linked by a generic property

我处于以下情况:

type NumericLiteral = {
  value: number;
  type: "NumericLiteral";
};
type StringLiteral = {
  value: string;
  type: "StringLiteral";
};
type Identifier = {
  name: string;
  type: "Identifier";
};
type CallExpression = {
  name: string;
  arguments: DropbearNode[];
  type: "CallExpression";
};

type DropbearNode =
  | NumericLiteral
  | StringLiteral
  | Identifier
  | CallExpression;

type Visitor = {
  [K in DropbearNode["type"]]: (node: Readonly<Extract<DropbearNode, { type: K }>>) => void
};

我想输入一个函数 visitNode,给定 DropbearNode 节点 nVisitor v 至少能够调用v[n.type]n 上:v[n.type](n)

我的第一次尝试是:

function visitNode<N extends DropbearNode>(node: Readonly<N>, v: Visitor) {
    v[node.type](node)
}

但 TS 似乎认为 v[node.type]node 完全无关,我不确定我是否理解到底出了什么问题。我的意思是,node.type 变成了 N["type"],被认为可以分配给完整联合 "NumericLiteral" | "StringLiteral" | "Identifier" | "CallExpression",所以最后我认为它期望所有节点类型的交集作为参数,即never 当然是因为他们形成了一个受歧视的联盟。这是怎么回事?

我的第二次尝试是:

function visitNode<K extends DropbearNode['type']>(node: Readonly<Extract<DropbearNode, { type: K }>>, v: Visitor) {
   v[node.type](node)
}

其中我试图在两件事之间强加一个 link。这似乎是个好主意,但我遇到了 design limitation 所以我不得不将 node.type 转换为 K 因为 TS 无法将 typeof node.type 减少到它。

那么,我怎样才能正确输入这个函数呢?第一次尝试失败的原因是什么?

TypeScript playground.

visitNode() 的实现中,v[node.type]node 都是 union types or generic types constrained 这样一个联合。

让我们将NumericLiteral等缩写为NLSL等,将(t: T)=>void缩写为Fn<T>。那么 v[node.type] 是可分配给 Fn<NL> | Fn<SL> | Fn<I> | Fn<CE> 的类型,而 node 是可分配给 NL | SL | I | CE 的类型。 如果编译器只知道 v[node.type]node 的类型 ,那么它不会让你毫无怨言地调用 v[node.type](node)。如您所知,函数类型的联合只能安全地接受 intersection of its parameter types (at least as of TypeScript 3.3's introduction of improved support for calling union types),而 NL | SL | I | CE 不能分配给 NL & SL & I & CE,因此会出现错误。

如果您编写 v[node1.type](node2),其中 node1node2 都是限制为 NL | SL | I | CE 的通用类型,那么这样的错误将是完全合理的。也许 node1NL 类型,但 node2SL 类型。在这种情况下,v[node1.type]node2 将属于 independent 联合类型。

但这不是正在发生的事情,是吗? v[node.type]node 显然 相互关联 。如果 nodeNL 类型,那么 v[node.type]Fn<NL> 类型。调用v[node.type](node)总是安全的,只是编译器看不到!

这个问题提示在 microsoft/TypeScript#30581. Up to and including TypeScript 4.5, the only ways to deal with this situation were either to use a type assertion 提交功能请求以仅抑制警告,或者编写冗余代码为 node 的每种可能类型制作一行。都不是很好。


幸运的是,TypeScript 4.6 引入了 improvements to generic indexed access inference as implemented in microsoft/TypeScript#47109 来解决这个问题。

诀窍是编写一个映射类型,其键为 DropbearNode['type'],其属性为关联的 node 类型:

type TypeMap = { [K in DropbearNode["type"]]: Extract<DropbearNode, { type: K }> }
/* type TypeMap = {
    NumericLiteral: NumericLiteral;
    StringLiteral: StringLiteral;
    Identifier: Identifier;
    CallExpression: CallExpression;
} */

然后,如果我们可以用 indexed access into this map, and if we can represent the type of v in terms of a similar mapped type 表示 node 的类型,其属性也根据相同的索引访问,编译器将准备好查看相关性:

type Visitor = {
  [K in keyof TypeMap]: (node: Readonly<TypeMap[K]>) => void
};

function visitNode<K extends keyof TypeMap>(node: Readonly<TypeMap[K]>, v: Visitor) {
  v[node.type](node) // okay
}

(请注意,这不是完全类型安全的;逆变位置的通用索引在 TS 中一直是不可靠的,即使 K 是一个联合,您也可以写入 T[K]。所以要小心. 请关注 ms/TS#48730 以获取此处的更新。)

万岁!


注意,不过...这是 finicky/fragile。如果您要将 Visitor 显式定义为一组属性,例如:

type Visitor = {
    NumericLiteral: (node: Readonly<NumericLiteral>) => void;
    StringLiteral: (node: Readonly<StringLiteral>) => void;
    Identifier: (node: Readonly<Identifier>) => void;
    CallExpression: (node: Readonly<CallExpression>) => void;
}

之前的相同错误会再次出现。即使此 Visitor 在结构上与映射类型相同,编译器也不再准备好关注相关性。带有 TypeMap[K] 的映射类型对于它的工作至关重要。所以要小心!


Playground link to code