Expected 3 type arguments but got 1 but it should infer 2 types

I’m wondering how to correctly infer the 2nd and 3rd type arguments of my function.

Suppose a simple interface:

interface ISome {
    a: string;
    b?: {
        c: string;
    };
}

The following works:

function pathBuilder<
    K1 extends keyof ISome,
    K2 extends keyof NonNullable<ISome[K1]>
>(p: K1, p2?: K2) {
    let res = String(p);
    if (p2) { res += "." + p2; }
    return res;
}

const pathTest = pathBuilder("b", "c"); // ---> "b.c" and intellisense works on parameters

But I need to generalize the function to work for a caller-defined type instead of ISome (I don’t want to pass an object instance to specify the type).

The following does not work:

function pathBuilder<
    T,
    K1 extends keyof T,
    K2 extends keyof NonNullable<T[K1]>
>(p: K1, p2?: K2) {
    let res = String(p);
    if (p2) { res += "." + p2; }
    return res;
}

const pathTest = pathBuilder<ISome>("b", "c"); // ERROR: Expected 3 type arguments, but got 1.ts(2558)

It seems that the 2nd and 3rd template arguments of the function don’t infer from the first, but it should because when I directly specified the type as T = ISome, it worked.

I’m not sure if there is some language keyword to make it work but the type parameter should work exactly for that: specifying an unknown type.

EDIT

Actually I found this way, but it requires extra coding I’d like to avoid if possible:

function pathBuilder<T>() {
    return <
        K1 extends keyof T,
        K2 extends keyof NonNullable<T[K1]>>(p: K1, p2?: K2) => {
        let res = String(p);
        if (p2) { res += "." + p2; }
        return res;
    };
}

const pathTest = pathBuilder<ISome>()("b", "c");

Simple and correct answer:

You can’t partially infer type parameters in TypeScript — if you explicitly pass one (<ISome>), you must provide the rest, unless the compiler can infer them.

To make it work without extra boilerplate, you need to reorder your generic parameters so that T is first and the others can be inferred:


Solution (Reorder generics to infer K1 and K2):

function pathBuilder<T, K1 extends keyof T, K2 extends keyof NonNullable<T[K1]>>(
  p: K1,
  p2?: K2
): string {
  let res = String(p);
  if (p2) { res += "." + p2; }
  return res;
}

// Explicitly specify only T — K1 and K2 will be inferred
const pathTest = pathBuilder<ISome>("b", "c"); //  Works fine

Why your original didn’t work:

TypeScript doesn’t support partial inference from left-to-right when you provide only the first generic parameter. Once you give <ISome>, the compiler expects you to provide all three, unless the rest can be inferred after it.


Summary:

  • :white_check_mark: Reorder generics: T, K1, K2
  • :white_check_mark: Call with <ISome> — the rest is inferred
  • :cross_mark: You can’t pass only 1 type argument if 3 are declared in order

Let me know if you want to support deeper paths like "b.c.d"!