The TypeScript inference algorithm for generic type arguments is effectively a set of heuristic rules which have been found to be useful in a wide range of real world code situations. The type checker tries to match two types, one of which contains some utterances of a generic type parameter like T
. It is possible that this matching process generates multiple candidate type arguments. For example, in the following function and call:
function f<T>(x: T, y: T) { }
f(a, b);
The compiler has two candidates for T
... it could be typeof a
, or typeof b
. When both types are the same, there's no issue:
f(1, 2); // okay, T inferred as number
f("one", "two"); // okay, T inferred as string
But what should the compiler do when the two candidates are different?
f(1, "two"); // what should happen here?
Here the compiler needs to do something with string
and number
. One might imagine that the compiler could synthesize a union type and infer that T
is string | number
. But that's not what happens.
It turns out that, in real world code, most people think that the above call should be rejected. The consensus is that f(1, "two")
isn't a valid call because 1
and "two"
are different types. So the compiler tends not to synthesize unions; instead it picks one candidate (usually the "first" one) and then checks the others against it:
f(1, "two"); // T inferred as string, error!
// ~~~~~ <-- Argument of type 'string' is not assignable
// to parameter of type 'number'.
See Why isn't the type argument inferred as a union type? for an answer by the TS Team development lead.
Unfortunately for your code, something very similar happens:
extract(getter); // error!
// function extract<string>(getter: () => string): string
The compiler needs to match the type () => M
against (() => string) | (() => number))
. This generates two candidates for M
: string
and number
. Because the compiler prefers not to synthesize unions in cases like this, it chooses string
and checks against that, causing the error.
So that's why it's happening.
There is an open feature request at microsoft/TypeScript#44312 asking for some way to annotate the type parameter so that the compiler will infer unions where necessary to prevent errors. Perhaps that would look like
// Not valid TS, don't do this:
declare function f<allowunion T>(x: T, y: T): void;
declare function extract<allowunion M>(getter: () => M): M;
but for now there is no such feature.
The easiest workaround here would be to give up on type argument inference and just specify the type argument yourself, manually. If you want M
or T
to be string | number
, just tell the compiler that:
f<string | number>(1, "two"); // okay
const result = extract<string | number>(getter); // okay
// const result: string | number;
There are doubtless other ways to change the call signature so that inference succeeds, but then your call signature will be more complicated and you'd need to compute your original M
type from it (e.g., the other answer here using ReturnType<T>
). It depends on your use cases whether you'd prefer to do something like that instead.
Playground link to code