I have a class representing a directed graph structure, which is generic with one type parameter K extends string
for the node names. A graph is constructed by passing an object like {a: ['b'], b: []}
which in this minimal example represents two nodes a and b, with one edge a → b.
class Digraph<K extends string> {
constructor(readonly adjacencyList: Record<K, K[]>) {}
getNeighbours(k: K): K[] {
return this.adjacencyList[k];
}
}
However, declared like this, the type parameter K
is inferred from the contents of the arrays, instead of from object's property names. This means K
becomes 'b'
instead of 'a' | 'b'
, and so Typescript gives an error because it thinks a
is an excess property in an object literal.
// inferred as Digraph<'b'> instead of Digraph<'a' | 'b'>
// error: Argument of type '{ a: string[]; b: never[]; }' is not assignable to parameter of type 'Record<"b", "b"[]>'.
let digraph = new Digraph({
a: ['b'],
b: [],
});
Is there a way to have K
inferred directly from the property names, instead of their values?
One solution I tried is to add another type parameter T extends Record<K, K[]>
and declare constructor(readonly adjacencyList: T) {}
. Then the excess property error goes away, but now K
is only inferred as string
.
Also, the type Digraph<K, T>
is too specific - two digraphs with the same nodes should be assignable to each other even when they have different edges, and I'd rather not have to write Digraph<K, Record<K, K[]>>
or Digraph<K, any>
to get around this. I'm looking for a solution which doesn't add an extra type parameter or change what K
would be, if possible.
So your problem is that there are multiple inference site candidates for K
in the type Record<K, K[]>
, and that the compiler's inference algorithm is giving priority to the wrong one. You would like to be able to tell the compiler that it should not use the second K
(in the elements of the array in the property value position) for inference, and that it should only use the first K
(in the property key position) for this purpose. It should only pay attention to that second site after K
is inferred, and only to check that the inferred type works.
TypeScript 5.4 has introduced a NoInfer<T>
utility type to support non-inferential type parameter usages. The type NoInfer<T>
eventually evaluates just to T
, but only after type inference has occurred. So you can write this:
class Digraph<K extends string> {
constructor(readonly adjacencyList: Record<K, NoInfer<K>[]>) { }
getNeighbours(k: K): K[] {
return this.adjacencyList[k];
}
}
and everything will just work:
let digraph = new Digraph({
a: ['b'],
b: [],
}); // okay, Digraph<"a" | "b">
let badDigraph = new Digraph({
a: ['c'], // error, "c" is not assignable to "a" | "b"
b: []
})