1

What should I do to make wrapper return string | number type? The point is that I need to have extract as a generic function to automatically recognize getter's result type. Now wrapper is considered to return string only. Thank you in advance.

function extract<M>(getter: () => M): M {
  return getter()
}

let getter: (() => string) | (() => number) 

function wrapper() {
  const result = extract(getter)
  //    ^                ^ it fails to match `string` with `number` here
  //    ^ I expect typeof result to be `string | number` but it is just `string` for some reason
  return result
}
Fox Amadeus
  • 196
  • 1
  • 1
  • 11
  • 1
    TypeScript doesn't like to synthesize unions to join multiple inference candidates, because often this isn't what people want (e.g., given `function f(x: T, y: T){}` most people expect `f(1, "two")` to be an error even though presumably `string | number` would work.) If you want such a union you can manually specify the type argument when calling `extract()` as shown [in this playground link](https://tsplay.dev/WYLbrm). Does that fully address the question? If so I could write up an answer with sources; if not, what am I missing? – jcalz Mar 04 '23 at 15:09
  • Thank you for the reply. Yes, explicitly specifying definitely works. However for me the background of this is still tricky. Could you please hint/point a documentation place that describes this as better as possible? – Fox Amadeus Mar 04 '23 at 18:07

3 Answers3

2

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

jcalz
  • 264,269
  • 27
  • 359
  • 360
1

I think the main issue is that you are telling TypeScript that the getter is a function that returns a string, or a function that returns a number. You could do it like this:

function extract<M>(getter: () => M): M {
  return getter()
}

let getter: () => string | number

function wrapper() {
  const result = extract(getter)
  return result
}

That works for me

Robin G
  • 307
  • 2
  • 12
  • Thank you. And what is the deep mechanics hidden there? Why does it work that way? – Fox Amadeus Mar 04 '23 at 14:14
  • I didn't know the internal reason, but I see a very good detailed explanation by [@jcalz](https://stackoverflow.com/a/75638312/12180422) below. – Robin G Mar 07 '23 at 10:59
0

Instead of using M to infer the return type of the function, try to infer the entire type of the function first, then use ReturnType as the return type:

function extract<F extends () => unknown>(getter: F): ReturnType<F> {
  return getter() as ReturnType<F>;
}

Playground

kelsny
  • 23,009
  • 3
  • 19
  • 48
  • Thank you. Is not this an anti-pattern since [the doc says](https://www.typescriptlang.org/docs/handbook/2/functions.html#push-type-parameters-down) to push the type down? – Fox Amadeus Mar 04 '23 at 18:11