Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Function argument types only inferred for mapped function with the maximum number of arguments #17391

Closed
grind086 opened this issue Jul 25, 2017 · 4 comments
Labels
Needs Investigation This issue needs a team member to investigate its status.

Comments

@grind086
Copy link

grind086 commented Jul 25, 2017

TypeScript Version: 2.4.1

Code

interface MyEvents {
    'zero'  : () => void;
    'one'   : (arg1: string) => void;
    'two'   : (arg1: string, arg2: string) => void;
    'three' : (arg1: string, arg2: string, arg3: string) => void;
    'four'  : (arg1: string, arg2: string, arg3: string, arg4: string) => void;
}

interface MyEmitter<T> {
    on<K extends keyof T>(event: K, fn: T[K]): this;
    once<K extends keyof T>(event: K, fn: T[K]): this;
}

declare const myEmitter: MyEmitter<MyEvents>;

myEmitter.on('zero'  , () => {});
myEmitter.on('one'   , (arg1) => {});
myEmitter.on('two'   , (arg1, arg2) => {});
myEmitter.on('three' , (arg1, arg2, arg3) => {});
myEmitter.on('four'  , (arg1, arg2, arg3, arg4) => {});

Expected behavior:
Types are inferred for all event callback arguments.

Actual behavior:
Types are inferred only for the callback arguments of the event 'four'. If 'four' is removed from MyEvents, then only the callback arguments for 'three' are inferred. If 'three' is removed, then only the arguments for 'two' are inferred. etc.

The order of properties on MyEvents has no effect, and if there are multiple events with the maximum number of arguments they are all inferred correctly.

EDIT: I just noticed adding another function with the same number of arguments, but different types also prevents arguments from being inferred, so this is probably an issue with merging the type signatures. Interestingly IntelliSense (both in VSCode, and the online playground) can still infer the proper types.

@jcalz
Copy link
Contributor

jcalz commented Aug 3, 2017

This does seem buggy to me. What's a minimal example of it?

interface Functions {
  's': (s: string) => void;
  'ns': (n: number, s: string) => void;
}
declare function getFunction<K extends keyof Functions>(key: K, func: Functions[K]): void;
getFunction('ns', (x, y) => { x.random; y.random }); // good errors, x is number, y is string
getFunction('s', (x) => { x.random }); // bad, x has any type?!
getFunction('s', (x: number) => { }); // good error, string is not assignable to number
getFunction('s', (x: string) => { }); // workaround, no error
getFunction('s', (x => {}) as Functions['s']); // workaround, no error

It's strange to me that the compiler does infer the right function type but does not infer the parameter types for that function. Obviously you can work around it by specifying the type on the parameter(s), but it seems eminently inferable to me. Is this a bug or a weird limitation?

@mhegazy mhegazy added the Needs Investigation This issue needs a team member to investigate its status. label Aug 29, 2017
@yseymour
Copy link

This is still an issue in version 3.2.0-dev.20180927

Repro:

interface FnMap {
  a: (x: string) => any;
  b: (x: number) => any;
}

declare function on<K extends keyof FnMap>(key: K, fn: FnMap[K]): void;

on('a', x => console.log(x));  // Bad: x is not inferred
on('b', x => console.log(x));  // Bad: x is not inferred

declare function handle_it(x: string): void;

on('a', handle_it);            // Good
on('b', handle_it);            // Good - Error

It looks like the actual type mapping is fine, since using the explicitly typed function produces the expected results, so the problem is with the type inference of the arguments of the anonymous functions.

@grind086
Copy link
Author

grind086 commented Sep 28, 2018

@yseymour Nice! Here's a slight variation on your example that I think provides some additional insight:

interface FnMap {
  a: (x: string) => any;
  b: (x: number, y: number) => any;
}

declare function on<K extends keyof FnMap>(key: K, fn: FnMap[K]): void;

on('a', x => console.log(x));          // Bad: x is not inferred
on('b', (x, y) => console.log(x, y));  // Good: x and y are inferred

I think the core of the problem might be that the callback provided for a (with implicitly typed x) is also valid for b. On the other hand, the callback provided for b expects more arguments than the expected callback for a, so it's only valid for b and inference works properly.

@RyanCavanaugh
Copy link
Member

This works now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

5 participants