If we aim to simplify this into a minimal reproducible example, consider the scenario where you have the function func
defined as follows:
declare function func<T>(this: T, key: keyof T): void;
You attempt to call it like this, but encounter an error:
func.call({ a: 123 }, "a") // error?!
// CallableFunction.call<{ a: number; }, [key: never], void>(...);
Why does this error occur?
The issue arises when trying to combine two generic functions with the expectation that the compiler will infer a relationship between them to pass on generic type parameters without explicit specification.
Although TypeScript 3.4 introduced support for some aspects of higher order type inference from generic functions, and TypeScript 3.5 extended this to include generic constructors, it falls short in cases like this due to heuristic limitations.
Had there been a stand-alone function call()
without a this
parameter, the desired behavior would work:
declare function call<T, A extends any[], R>(
thisArg: T, cb: (this: T, ...args: A) => R, ...args: A): R;
call({ a: 123 }, func, "a") // okay
call({ a: 123 }, func, "b") // error
But why doesn't it work as expected with a this
parameter?
The reason lies in the fact that current support for higher order function inference is heuristic and not comprehensive. The existing algorithm, while imperfect, serves performance goals, as highlighted in microsoft/TypeScript#30215.
The above algorithm is not a complete unification algorithm and it is by no means perfect.
Achieving full unification, as discussed in microsoft/TypeScript#30134, would necessitate substantial changes to TypeScript's type inference mechanism and might impact compiler performance. For now, the algorithm remains imperfect yet efficient.
While investigating, I came across microsoft/TypeScript#33139, a pull request promising improved inference with this
parameters. Despite being under the lead language architect's purview, its status has remained stagnant for some time without associated bug reports or feature requests.
To address this, an immediate workaround involves manually specifying the generic type parameters:
func.call<{ a: number }, [key: keyof { a: number }], void>({ a: 123 }, "a"); // okay
Although it's not ideal, at least such manual specifications are recognized by the compiler for func.call
, eliminating the need for unsafe type assertions.
In the long run, providing feedback on microsoft/TypeScript#30215 regarding the benefits of a full unification algorithm could be beneficial. Additionally, advocating for extending TypeScript 3.4's support for higher-order functions to cover this
parameters through a separate feature request linked to microsoft/TypeScript#33139 might prompt consideration, though outcomes aren't guaranteed.
Playground link to code