microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.84k stars 12.46k forks source link

Regression in distributed conditional types from 4.7 onward #52152

Closed kevinlul closed 1 year ago

kevinlul commented 1 year ago

Bug Report

🔎 Search Terms

"distributed conditional types"

🕗 Version & Regression Information

⏯ Playground Link

Playground link with relevant code

💻 Code

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}`))

🙁 Actual behavior

The indicated line fails to compile in TypeScript 4.7, 4.8, and 4.9, when it had compiled in TypeScript 4.6. Making the log statement not a return statement with curly braces, explictly typing event: CloseEvent, or dropping the second argument to the lambda function fixes the issue.

🙂 Expected behavior

All statements should compile.

Andarist commented 1 year ago

More minimal repro case: TS playground

RyanCavanaugh commented 1 year ago

For the bot

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

declare class Client {
  public on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): void;
}

// Demonstrative code
const bot = new Client();
// two declared params
bot.on("shardDisconnect", (event, shard) => console.log(`Shard ${shard} disconnected (${event.code},${event.wasClean}): ${event.reason}`));
// single declared param
bot.on("shardDisconnect", event => console.log(`${event.code} ${event.wasClean} ${event.reason}`))
typescript-bot commented 1 year ago

The change between origin/release-4.5 and origin/release-4.8 occurred at fd601ddf20af2a69e56d69fe418d0f80905ce66b.