Q:

TypeScript function overloads

In an effort to reduce the cognitive load on my aging gray matter, I'm providing custom overloads for specific events in an event emitter.
// default signature
function on(event: string | symbol, listener: (...args: any[]) => void): void;
// custom signatures
function on(event: "error", listener: (error: string, code: number) => void): void;
function on(event: "data", listener: (text: string) => void): void;
function on(event: "chunk", listener: (text: string) => void): void;
function on(event: "complete" | "done", listener: (id: number) => void): void;
// most general signature
function on(event: any, listener: (...args: any[]) => void) {
  // ...
}

// correctly resolve data as 'string'
on("data", (data) => {});
on("chunk", (data) => {});
// incorrectly resolves id as any from default signature
on("complete", (id)  => {});

Per the TypeScript docs, I've ordered general overloads after specific overloads.
My question is; why does the union type ("complete" | "done") not work as expected but separating them ("data" and "chunk") does?

A:

From the documentation on overloads:

In order for the compiler to pick the correct typecheck, it follows a similar process to the underlying JavaScript. It looks at the overload list, and proceeding with the first overload attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload. For this reason, it’s customary to order overloads from most specific to least specific.

I do see that sometimes the compiler does choose overloads "out of order" by looking for more specific signatures even when they are not the first matching signature in the list (e.g., see GitHub issue Microsoft/TypeScript#9443)... but when in doubt, you'll get the best behavior if you list your overloads from the most specific to the most general.  For example:
// custom signatures
function on(event: "error", listener: (error: string, code: number) => void): void;
function on(event: "data", listener: (text: string) => void): void;
function on(event: "chunk", listener: (text: string) => void): void;
function on(event: "complete" | "done", listener: (id: number) => void): void;
// default signature
function on(event: string | symbol, listener: (...args: any[]) => void): void;
// implementation signature
function on(event: any, listener: (...args: any[]) => void) {
  // ...
}

This should work how you want.  Hope that helps.  Good luck!

