1

I am trying to create type information for my event listeners. As all event listeners are set on the same .on() function, I am using generics.

type Name = "error" | "connected";

type Callback = {
    error: (err: Error) => void,
    connected: (err: number) => void,
};

function on<T extends Name>(eventName: T, callback: Callback[T]): void { }

on("error", (err) => err.stack);
on("connected", (err) => err.stack);

I would expect the above to give me an error for the connected event as I attempt to use a number as an Error, however I get no type hinting at all for my callback functions.

However if all function definitions in Callback match, it does begin to work. As below:

type Callback = {
    error: (err: Error) => void,
    connected: (err: Error) => void,
};

GIF of what I mean from VS code:

GIF

Am I doing something wrong?

2 Answers 2

2

This seems to be a strange behavior on behalf of the inference engine in the compiler.

Speculation on why: If you type the second parameter, as Callback[T], the engine will try to determine T based on the argument type. So if you don't explicitly specify the type for the arrow function, the inference engine will infer the parameter for the arrow function to be any and try to guess T based on the arrow function type. (If you run with strict you will actually receive an error that the parameter has type any implicitly).

There are two possible solutions:

Use a two function approach, where T is determined in the first call and is known for the second call where the parameter is passed:

type Callback = {
    error: (e: Error) => void
    connected: (e: string) => void
};
function on<T extends Name>(eventName: T) { 
    return function(callback: Callback[T])
    {
    };
}

on("error")(e=> e.stack);
on("connected")(e=> e.substr(1));

If the only thing that differs between functions is the parameter type and you are using 2.8 or older (unreleased at the time of writing but it is in RC, you can get it via npm install -g typescript@rc) you can get just the argument that differs, and the inference engine will not try to use the second parameter to infer T

type Arg0<T> =  T extends (p1: infer U) => any ? U: never;
function on<T extends keyof Callback>(eventName: T, callback: (e: Arg0<Callback[T]>) => void) : void { }

on("error", e=> e.stack);
on("connected", e=> e.substr(1));
Sign up to request clarification or add additional context in comments.

1 Comment

Cool didn't know that keyof trick :). Specifying the type when calling the function does work. Your speculation sounds plausible, but any thoughts on why it works without specifying type when all Callback function definitions match?
1

Seems you doing over engineering things.

Event can be listed in some enum.

enum EVENTS{
  ERROR = "ERROR",
  CONNECTED = "CONNECTED"
}

on method can be not generic

function on(eventName: EVENTS, callback: (v: any) => void): void { }

And just few examples

on(EVENTS.ERROR, (v: any) => {
  if(v instanceof Error){
    console.log(v.stack);
  }
});

on(EVENTS.CONNECTED, (v:any) => {
  console.log(v);
});

As usual it is hard to set type of event value.

Sorry. Here is edited function.

function on<T>(eventName: EVENTS, callback: (v: T) => void): void { }

on<Error>(EVENTS.ERROR, (err) => {  
    console.log(err.stack);  
});

on<any>(EVENTS.CONNECTED, (v) => {
  console.log(v);
});

1 Comment

This doesn't really give me what I want. There is no guarantee that the callback function accepts a single parameter and I do not wish to have to specify the <type> every time I call .on(). I want the type inferred from the first parameter passed to .on() and I want the callback information also inferred from that type.

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.