I'm trying something like the following:
type Node =
| { type: 'Group'; children: Node[] }
| { type: 'Obj'; payload: number };
class Group {
readonly type = 'Group';
constructor(public n: Node[]) { }
}
class Obj {
readonly type = 'Obj';
constructor(public p: number) { }
}
type NodeMappings =
{ Group: Group; Obj: Obj };
function createNode<T extends Node>(node: T): NodeMappings[T['type']] {
switch (node.type) {
case 'Group':
return new Group(node.children); // error!
case 'Obj':
return new Obj(node.payload); // error!
}
}
Is there a way to achieve something like the given code that works with all kinds of types for the mapping in the createNode function (here done via a switch ... case)? Is this a bug in Typescript?
The compiler cannot just see the abstract relationship between the
NodeandNodeMappingsin order to verify thatcreateNode()is properly implemented.If you want to express that relationship in a way the compiler understands, you need to do so explicitly by following the steps laid out in microsoft/TypeScript#47109. Your need a "base" type which looks like a key-value mapping from each
typeproperty to the rest of the relevantNodemember:which is equivalent to
And then all the operations should be represented in terms of that base type and indexes into that type with a generic key, or generic indexes into a mapped type on that type.
We can recreate
Nodeas such a indexed mapped type (called a distributive object type in microsoft/TypeScript#47109):and then
createNode()is written in terms of the generic indexKandMyNode<K>:Note how we had to abandon the control-flow based implementation with
switch/case. You can't just checknode.typeand expectKto be affected, at least until and unless microsoft/TypeScript#33014 is implemented. So instead of doing this, we just make an object whose methods have the same names asnode.typeand which accept the corresponding node as input. That is, we write an object that mapsMyNode<K>toNodeMappings[K].And this works; the call
m[node.type](node)is effectively the same as yourswitch/caseversion, but we use property lookups to distinguish between the cases.And let's make sure it still works for callers:
Looks good.
Kis inferred as"Obj"and thus the return type isObjas expected.Playground link to code