如何关联由通用 属性 链接的不同参数
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
节点 n
和 Visitor
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
减少到它。
那么,我怎样才能正确输入这个函数呢?第一次尝试失败的原因是什么?
在 visitNode()
的实现中,v[node.type]
和 node
都是 union types or generic types constrained 这样一个联合。
让我们将NumericLiteral
等缩写为NL
、SL
等,将(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)
,其中 node1
和 node2
都是限制为 NL | SL | I | CE
的通用类型,那么这样的错误将是完全合理的。也许 node1
是 NL
类型,但 node2
是 SL
类型。在这种情况下,v[node1.type]
和 node2
将属于 independent 联合类型。
但这不是正在发生的事情,是吗? v[node.type]
和 node
显然 相互关联 。如果 node
是 NL
类型,那么 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]
的映射类型对于它的工作至关重要。所以要小心!
我处于以下情况:
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
节点 n
和 Visitor
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
减少到它。
那么,我怎样才能正确输入这个函数呢?第一次尝试失败的原因是什么?
在 visitNode()
的实现中,v[node.type]
和 node
都是 union types or generic types constrained 这样一个联合。
让我们将NumericLiteral
等缩写为NL
、SL
等,将(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)
,其中 node1
和 node2
都是限制为 NL | SL | I | CE
的通用类型,那么这样的错误将是完全合理的。也许 node1
是 NL
类型,但 node2
是 SL
类型。在这种情况下,v[node1.type]
和 node2
将属于 independent 联合类型。
但这不是正在发生的事情,是吗? v[node.type]
和 node
显然 相互关联 。如果 node
是 NL
类型,那么 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]
的映射类型对于它的工作至关重要。所以要小心!