为什么这段代码从 TypeScript 4.7 开始无法编译?

huangapple go评论72阅读模式
英文:

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

问题

我有一段代码片段,之前在 TypeScript 4.6 中能够编译通过,但从 TypeScript 4.7 开始(4.7、4.8、4.9),就无法编译通过了。问题在于,如果涉及的语句不是返回语句,或者 lambda 函数只有一个参数而不是两个,则问题得到解决。

Playground 链接

import { EventEmitter } from 'node:events';

// 简化自 discord.js https://github.com/discordjs/discord.js 许可证 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;
}

// 示范性代码
const bot = new Client();
// 返回语句。无法编译,认为 event 是所有第一个参数的联合类型(string | CloseEvent),但悬停在 `event` 上只显示 CloseEvent。
bot.on("shardDisconnect", (event, shard) => console.log(`Shard ${shard} disconnected (${event.code},${event.wasClean}): ${event.reason}`));
// 不是返回语句。可以编译。
bot.on("shardDisconnect", (event, shard) => {
  console.log(`Shard ${shard} disconnected (${event.code},${event.wasClean}): ${event.reason}`);
});
// 返回语句。可以编译。
bot.on("shardDisconnect", event => console.log(`${event.code} ${event.wasClean} ${event.reason}`))
英文:

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

答案1

得分: 3

看起来你可能遇到了与分布式条件类型相关的潜在错误。ClientEvents[keyof ClientEvents] 的类型是 [message: string] | [closeEvent: CloseEvent, shardId: number]。尽管 IDE 看起来正确地意识到你的参数应该是 event, shardId,因为你已将 K 判定为 shardDisconnect,但在某个时候解析器失去了这个判定,并认为第一个参数的类型是 string | CloseEvent(这是来自 warn 和 shardDisconnect 的第一个参数的联合类型)。

似乎你可以通过使用条件类型作为参数类型来解决这个问题,这样正确地重新缩小了 K

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

但是,如果你正在使用条件类型,你可以简化你的重载:

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;  
}

至于回调是否返回 void 或不返回 void 如何改变行为,我完全没有答案。这似乎是一个值得在 TypeScript 项目中提出问题的地方。

Playground 链接

英文:

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&lt;K extends keyof ClientEvents&gt;(event: K, listener: (...args: K extends keyof ClientEvents ? ClientEvents[K] : any[]) =&gt; Awaitable&lt;void&gt;): this;

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

declare class Client extends BaseClient {
  public on&lt;K extends string| symbol&gt;(event: K, listener: (...args: K extends keyof ClientEvents ? ClientEvents[K] : any[]) =&gt; Awaitable&lt;void&gt;): 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

huangapple
  • 本文由 发表于 2023年1月9日 03:34:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/75050705.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定