Search code examples
typescriptgenericstype-inference

How to infer type parameter from keys instead of values?


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?

Playground Link


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.


Solution

  • 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: []
    })
    

    Playground link to code