Home > Enterprise >  Why does this code fail to compile from TypeScript 4.7 onward?
Why does this code fail to compile from TypeScript 4.7 onward?

Time:01-09

I have a code snippet that was previously compiling in TypeScript 4.6, but fails to compile from TypeScript 4.7 onward (4.7, 4.8, 4.9). The issue is resolved if the statement in question is not a return statement, or if the lambda function only has one argument instead of both.

Playground link

import { EventEmitter } from 'node:events';

// Simplified from discord.js https://github.com/discordjs/discord.js licence Apache 2.0
interface ClientEvents {
  warn: [message: string];
  shardDisconnect: [closeEvent: CloseEvent, shardId: number];
}

class BaseClient extends EventEmitter {
  public constructor() {
    super();
  };
}

type Awaitable<T> = PromiseLike<T> | T;

declare class Client extends BaseClient {
  public on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => Awaitable<void>): this;
  public on<S extends string | symbol>(
    event: Exclude<S, keyof ClientEvents>,
    listener: (...args: any[]) => Awaitable<void>,
  ): this;
}

// Demonstrative code
const bot = new Client();
// Return statement. Fails to compile, thinks that event is a union type of all first arguments (string | CloseEvent), but hovering over `event` shows just CloseEvent.
bot.on("shardDisconnect", (event, shard) => console.log(`Shard ${shard} disconnected (${event.code},${event.wasClean}): ${event.reason}`));
// Not a return statement. Compiles.
bot.on("shardDisconnect", (event, shard) => {
  console.log(`Shard ${shard} disconnected (${event.code},${event.wasClean}): ${event.reason}`);
});
// Return statement. Compiles.
bot.on("shardDisconnect", event => console.log(`${event.code} ${event.wasClean} ${event.reason}`))

CodePudding user response:

It looks like you're running into a possible bug related to distributed conditional types. The type of ClientEvents[keyof ClientEvents] is [message: string] | [closeEvent: CloseEvent, shardId: number]. While the IDE seems to correctly realize that your args should be event, shardId since you've discriminated K down to shardDisconnect, at some point the parser loses that discrimination and thinks that first arg is of type string | CloseEvent (that being the union of the first args from warn and shardDisconnect).

It appears that you can work around this by using a conditional type as the argument type, which correctly re-narrows K:

public on<K extends keyof ClientEvents>(event: K, listener: (...args: K extends keyof ClientEvents ? ClientEvents[K] : any[]) => Awaitable<void>): this;

But, if you're using a conditional type, you can just get rid of your overload:

declare class Client extends BaseClient {
  public on<K extends string| symbol>(event: K, listener: (...args: K extends keyof ClientEvents ? ClientEvents[K] : any[]) => Awaitable<void>): this;  
}

I have absolutely no answer for why the callback returning void or not alters the behavior, though. This smells like something that might be worth opening as an issue on the Typescript project.

Playground link

  • Related