The connection between the Node
and NodeMappings
cannot be automatically understood by the compiler to ensure that createNode()
is correctly implemented.
To clarify this relationship for the compiler, you must explicitly define it by following the guidelines outlined in microsoft/TypeScript#47109. You need to establish a "base" type that resembles a key-value mapping from each type
property to the other relevant members of Node
:
type NodeMap = { [N in Node as N['type']]:
{ [P in keyof N as P extends "type" ? never : P]: N[P] }
}
this can be equated to
// type NodeMap = {
// Group: { children: Node[]; };
// Obj: { payload: number; };
// }
Subsequently, all operations should be framed in relation to that base type and accessed through indexes using either generic keys, or generic indexes within a mapped type on that type.
We can redefine Node
as an indexed mapped type (referred to as a distributive object type in microsoft/TypeScript#47109):
type MyNode<K extends keyof NodeMap = keyof NodeMap> =
{ [P in K]: { type: P } & NodeMap[P] }[K]
and subsequently, createNode()
is formulated based on the generic index K
and MyNode<K>
:
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
}
It is important to note that we have moved away from the control-flow based implementation involving switch
/case
. Merely checking node.type
does not alter K
, unless changes are made to microsoft/TypeScript#33014. Therefore, instead of relying on this method, we create an object with methods mirroring the names of node.type
and accepting the respective node as input. Essentially, we construct an object that maps MyNode<K>
to NodeMappings[K]
.
This approach works effectively; the invocation m[node.type](node)
mirrors the behavior of the traditional switch
/case
structure, but utilizes property lookups to distinguish between cases.
Furthermore, let's ensure that callers still experience seamless functionality:
function test() {
const n = createNode({ type: 'Obj', payload: 8 });
// ^? const n: Obj
}
Everything appears to be functioning as intended. The inference of "Obj"
for K
results in a return type of Obj
, validating the integrity of the operation.
Access the code via Playground link