thorn thorn - 1 year ago 85
TypeScript Question

Type inference for functions passed to generic function wrappers like _.debounce

In TypeScript, if a function expression is passed as an argument, the types of its parameters are inferred perfectly:

var foo = (fn: (a: string) => void) => {};

foo(a => {
// a is inferred to be a string

It's really convenient for event handlers and other callbacks. However, if the function expression is wrapped in a call of a generic wrapper function, which takes a function as a parameter and returns a function with the same signature (except for return value), e.g.
of Lodash, the inference doesn't happen.

var debounce = <T>(fn: (a: T) => void) => {
return (a: T) => { /* ... */ };

foo(debounce(a => {
// a is inferred to be {}
// type error: 'toLowerCase' doesn't exist on '{}'

TS Playground

wants its argument to be
(a: string) => void
, the compiler could try to find such a type for
would return
(a: string) => void
. But it doesn't try to do this.

Am I missing something? Should I have written the type annotations for
somehow else? Is it by design? Is there an issue on GitHub about this case?

Answer Source

Firstly, when you call a generic function without type arguments, the compiler has to figure out the type for each type parameter.

So when you call debounce, the compiler has to find candidates for the type of T. But the only place debounce can draw inferences from is your function, and you didn't give an explicit type for your type parameter.

So the compiler figures it doesn't have any types to work with, and falls back to {} (the "empty type", often just called "curly curly") for T.

That whole process is called type argument inference.

What then happens is that the compiler will notice that you haven't given your arrow function's parameters a type. Rather than defaulting to any, it figures that it can figure it out from the type of debounce's parameters. This process is called contextual typing.

Well fn's type is basically (a: {}) => void, so the compiler figures "alright, great, we can give a a type!" which ends up being... {} unfortunately.

So while this isn't totally absolutely awesome, the fix isn't really that bad. Just add a type annotation for a:

foo(debounce((a: string) => { 
    // Everything is fine!

or use a type parameter:

foo(debounce<string>(a => { 
    // Everything is fine!