One common limitation of TypeScript is its inability to always infer generic type arguments and the contextual type of an expression simultaneously, especially when there is potential for circular dependencies between them in syntax. TypeScript's inference algorithm does not utilize full unification, resulting in instances where it may fail to infer as expected. To address this, you can either annotate callback parameters or manually specify generic type arguments.
For instance, consider the following function signature:
function run<U>(func1: (arg: U) => void, func2: (arg: { a: string }) => U): void {}
When calling this function with:
run((params) => {}, () => 23);
the inference works correctly because 'U' can be inferred from the return type of '() => 23,' which is not context-sensitive. On the other hand, calling it with:
run((params) => {}, (arg) => 23);
does not behave as desired due to the context-sensitivity of '(arg) => 23.' In such cases, TypeScript defers evaluation, leading to inference failure and fallback to 'unknown.'
The aforementioned issue has sparked discussions within the TypeScript community, with improvements being made in recent versions. For example, TypeScript 4.7 introduced enhanced function inference for objects and methods, facilitating better type inference in certain scenarios.
By rearranging function parameters, you can leverage this improved inference mechanism to achieve the desired results:
function run2<U>(
func2: (arg: { a: string }) => U,
func1: (arg: U) => void
): void { }
run2(() => 23, (params) => { });
run2((arg) => 23, (params) => { });
This approach enables inference flow from left-to-right, addressing some of the challenges faced in inference scenarios. Ultimately, how you proceed depends on your specific use case and constraints.
Playground link to code