3

I'm trying to understand why Typescript is able to infer the return type of a function with a callback parameter when the generic is the return type of the callback, but not able to do so when the generic is the type of the callback function.

There is no specific inference with

const f = <T extends () => any>(callback: T) => callback()

// inference r: any
const r = f(() => 1)

But the inference works with

const f = <T extends any>(callback: () => T) => callback()

// inference r: number
const r = f(() => 1)

1 Answer 1

3

I think this is a simplification the compiler makes when performing certain operations on generic types. Instead of representing every operation as a possibly increasingly complex generic type, it widens the generic type parameter to its constraint and uses that. You can see this happen when you index into a generic-typed object with a key it's known to have:

function foo<T extends { a: any }>(obj: T) {
  const a1 = obj.a; // any, why not T['a']?
  const a2: T['a'] = obj.a; // this works though
}

See microsoft/TypeScript#33181 for more information. In the above, the compiler sees obj.a and widens obj from T to {a: any} before accessing its a property. So a1 is of type any. If the compiler had instead deferred the widening, it could have represented this property as the lookup type T['a']. And indeed, if you explicitly annotate the variable you're saving it to as T['a'], the compiler does not complain.

The same seems to happen for calling a function of a generic type (although I haven't found canonical documentation mentioning this):

function bar<T extends () => any>(fn: T) {
  const r1 = fn(); // any, why not ReturnType<T> ?
  const r2: ReturnType<T> = fn(); // this works though
}

As you can see, r1 is of type any because the compiler widens fn from T to its constraint, () => any, before it is called. If the compiler had instead deferred the widening, it could have represented the return type as ReturnType<T> (see documentation for ReturnType). And again, if you manually annotate the value as ReturnType<T>, the compiler does not complain about it.

This leads me to what I think is the right solution/workaround for you: manually annotate the return type of your function:

const f = <T extends () => any>(callback: T): ReturnType<T> => callback()

That compiles with no error, and now when you call f on a callback you get a better return type:

const r = f(() => 1); // number

Playground link to code

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the detailed answer. What is the reason for TypeScript making this simplification? Is it just for faster compilation?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.