Search code examples
typescript

How can one specify some generic type arguments while having the others inferred?


Given the example below, are there any ways to achieve DRY code that's similar to foobarA?

interface F<T, U extends string> { t: T, f: (u: U) => void }
declare const foo: <T, U extends string>(type: U) => F<T, U>;

// the following correctly types foobar, however 'bar' redundantly repeats.
const foobar = foo<string, 'bar'>('bar')

// ideally one would want the generic inference to be smarter and the
// following to work:
const foobarA = foo<string>('bar')

Solution

  • TypeScript does not directly support partial type argument inference as requested in microsoft/TypeScript#26242. So if you have a generic function of the form:

    declare const foo: <T, U extends string>(u: U) => F<T, U>;
    

    there is no way to call it so that you manually specify T but let the compiler infer U:

    // const foobarA: F<string, unknown> ☹
    const foobar = foo<string, "bar">("bar");
    

    Either you have to manually specify both type arguments as in foo<string, "bar">("bar"); (which is unfortunately redundant) or you let the compiler infer both arguments as in foo("bar") (which doesn't infer the right type for T).

    Until and unless this changes, you will need to work around it.


    One workaround is currying, whereby you split the single generic function call into multiple generic function calls, so that you can manually specify the type argument for the first, and then let the compiler infer the type argument for the second. That means we'd need to refactor the above foo() into

    declare const foo: <T, >() => <U extends string>(u: U) => F<T, U>;
    

    And then we could call the function like this:

    declare const foo: <T, >() => <U extends string>(u: U) => F<T, U>;
    const foobarA = foo<string>()("bar");
    // const foobarA: F<string, "bar">
    

    It's a bit annoying to have to throw in that extra function call, but syntactically at least it's just another pair of parentheses, and it gives you exactly the behavior you're looking for.


    Another workaround is to add a dummy parameter corresponding to the type argument you'd like to specify manually, and then pass in a value of that type so that the compiler can successfully infer it. That looks like this:

    declare const foo: <T, U extends string>(dummyT: T, u: U) => F<T, U>
    

    And then we could call the function like this:

    const foobarA = foo("someString", "bar");
    // const foobarA: F<string, "bar">
    

    The value "someString" passed in for dummyT is almost certainly ignored by the function implementation, but it allows the compiler to infer string for T. If the caller doesn't have a value of the type handy, they can still use the dummy version by asserting that something random is of that type:

    const foobarB = foo(null! as string, "bar");
    // const foobarB: F<string, "bar">
    

    Here I've used null!, the value is null and by asserting that it is non-null via postfix !, we have an impossible "non-null null" of the never type which can be assigned to any other type. The main benefit of a mind-bending thing like null! is that it takes only five keystrokes to produce a value of the impossible never type, which can be asserted as string without complaint by the compiler.


    Both versions work; I tend to favor currying unless the use case makes it easy for the caller to find and pass in an appropriate argument for the dummy parameter.

    Playground link to code