简体   繁体   中英

Function Argument Type Inference through Generics behaves weirdly

Have a look at this simple example code:

interface EventEmitter<ListenersT>
{
    on<EventT extends keyof ListenersT>(event: EventT, listener: ListenersT[EventT]): this;
}

interface MyEvents
{
    foo(x: number): void;
    bar(): void;
    moo(a: string, b: Date): void;
}

interface MyEmitter extends EventEmitter<MyEvents>
{}

const my: MyEmitter = <any> {};

my.on('foo', (x) => x / 4); // Parameter 'x' implicitly has an 'any' type.
my.on('bar', () => 42);
my.on('moo', (a, b) => a + ': ' + b.getFullYear());

I am wondering why in the my.on('foo', ...) call the compiler fails to infer the type of x , while it works fine for both arguments in the my.on('moo', ...) call.

You can go and test this on the Typescript Playground . You can enable the noImplicitAny flag by pressing the Options button to get the warning I put in the comment or just hover over the x argument in the code window to see that it failed to infer the number type.

Bonus: This gets even weirder, if I add another argument to foo() . In that case it also fails to infer the types for moo 's arguments.

Edit 1

It appears it's only able to infer the types for the function in MyEvents with the most arguments and only if it's the only one with that amount of arguments.

Typescript has trouble inferring the arguments to callbacks when the type of the callback depends on another parameter, I have seen this in several SO questions (I can search for some if you want).

The solution is to put the problem in a different way, that is to make the on function have several overloads. The trick is to do this without having to restate all signatures from the interface.

Your requirements are very similar to this answer, and you will find a more detailed explanation there, but modified to your needs the code would look something like this:

interface EventEmitter<ListenersT>
{
    on : OnAll<ListenersT, this>
}

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type OnSignatures<T, TReturn> = { [P in keyof T]: (event: P, listener: T[P]) => TReturn }
type OnAll<T, TReturn> = UnionToIntersection<OnSignatures<T, TReturn>[keyof T]>

interface MyEvents
{
    foo(x: number): void;
    bar(): void;
    moo(a: string, b: Date): void;
}

interface MyEmitter extends EventEmitter<MyEvents>
{
    // on is now equivalent to an on with these overloads
    //on(event: 'foo', listener: (x: number) => void): this;
    //on(event: 'bar', listener: () => void): this;
    //on(event: 'moo', listener: (a: string, b: Date)=> void): this;
}

const my: MyEmitter = <any> {};

my.on('foo', (x) => x / 4); //  x is number 
my.on('bar', () => 42);
my.on('moo', (a, b) => a + ': ' + b.getFullYear());

Playground link

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM