编译器不能仅仅看到Node
和NodeMappings
之间的抽象关系来验证createNode()
是否正确实现.
如果你想以编译器可以理解的方式来表达这种关系,你需要按照microsoft/TypeScript#47109中的步骤来明确地做到这一点.你需要一个"base"类型,它看起来像是从每个type
属性到相关Node
成员的其余部分的键值映射:
type NodeMap = { [N in Node as N['type']]:
{ [P in keyof N as P extends "type" ? never : P]: N[P] }
}
这相当于
// type NodeMap = {
// Group: { children: Node[]; };
// Obj: { payload: number; };
// }
然后,所有操作都应该表示为该基本类型,并使用generic键将indexes表示为该类型,或者将通用索引表示为该类型上的mapped type.
我们可以将Node
重新创建为这样一个索引映射类型(在Microsoft/TypeScript#47109中称为distributive object type):
type MyNode<K extends keyof NodeMap = keyof NodeMap> =
{ [P in K]: { type: P } & NodeMap[P] }[K]
然后按照通用索引K
和MyNode<K>
写出createNode()
:
function createNode<K extends keyof NodeMap>(node: MyNode<K>) {
const m: { [K in keyof NodeMappings]: (n: MyNode<K>) => NodeMappings[K] } = {
Group(n) { return new Group(n.children) },
Obj(n) { return new Obj(n.payload) }
};
return m[node.type](node); // okay
}
请注意,我们不得不放弃使用switch
/case
的基于control-flow的实现.您不能只判断node.type
并期望K
会受到影响,至少在microsoft/TypeScript#33014实现之前是这样的.因此,我们不这样做,而是创建一个对象,该对象的方法与node.type
同名,并且接受相应的 node 作为输入.也就是说,我们编写了一个将MyNode<K>
映射到NodeMappings[K]
的对象.
这是可行的;调用m[node.type](node)
实际上与您的switch
/case
版本相同,但我们使用属性查找来区分情况.
让我们确保它仍然适用于呼叫者:
function test() {
const n = createNode({ type: 'Obj', payload: 8 });
// ^? const n: Obj
}
看起来不错 K
被推断为"Obj"
,因此返回类型为Obj
.
Playground link to code个