You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript泛型位置使用展开运算符类型报错解决方案

问题背景

在TypeScript项目中对接第三方事件客户端时,尝试在可变参数位置展开事件回调参数,因第三方提供的Client、ClientEvents类型无法修改,业务代码编译时抛出类型错误。
初始实现代码如下:

interface ClientEvents {
  ready: [client: string]
  interactionCreate: [interaction: number]
}

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

  public once<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => Awaited<void>): this;
  public once<S extends string | symbol>(
    event: Exclude<S, keyof ClientEvents>,
    listener: (...args: any[]) => Awaited<void>,
  ): this;
}

type MyEvent<T extends keyof ClientEvents> = {
  name: T,
  execute: (...args: [...args: ClientEvents[T], commands: string]) => Awaited<void>
}

declare const readyEvent: MyEvent<"ready">
declare const interactionCreateEvent: MyEvent<"interactionCreate">
declare const events: {
  ready: typeof readyEvent,
  interactionCreate: typeof interactionCreateEvent
}

declare const client: Client

Object.values(events).forEach((event) =>
  client[event.name === "ready" ? "once" : "on"](event.name, (...args) =>
    event.execute(...args, "test")
  )
)

报错信息

编译时抛出如下类型错误:

(parameter) args: [client: string] | [interaction: number]
Argument of type 'string | number' is not assignable to parameter of type 'never'.
  Type 'string' is not assignable to type 'never'.
报错原因

TypeScript无法在forEach遍历的联合类型上下文中,自动建立event.name和event.execute参数列表的关联关系:

  • 遍历得到的event被推断为MyEvent<"ready"> | MyEvent<"interactionCreate">联合类型
  • 回调参数args被推断为所有事件参数的联合[client: string] | [interaction: number]
  • 对联合类型的元组做展开操作时,TS会将元素类型做交叉计算,string & number的结果为never,最终触发传参类型不匹配错误。
解决方案

以下方案均不需要修改第三方库的类型定义,仅调整业务代码即可解决问题。

方案1:泛型函数封装(推荐,类型安全无额外断言)

提取单独的泛型事件注册函数,利用泛型参数绑定单个事件的名称和参数对应关系,TS可以在泛型函数上下文内正确推导参数类型:

// 单个事件注册的泛型函数
function registerEvent<K extends keyof ClientEvents>(event: MyEvent<K>) {
  const listenerMethod = event.name === "ready" ? "once" : "on";
  client[listenerMethod](event.name, (...args: ClientEvents[K]) => {
    event.execute(...args, "test");
  });
}

// 遍历注册所有事件
Object.values(events).forEach(event => registerEvent(event));

方案2:分支判断收窄类型(逻辑直观,无额外封装)

在遍历内部通过判断事件名做分支处理,TS可以通过类型守卫自动收窄每个分支内的event类型,不需要额外泛型封装:

Object.values(events).forEach((event) => {
  if (event.name === "ready") {
    // 此分支内event被收窄为MyEvent<"ready">,args类型为[string]
    client.once(event.name, (...args) => event.execute(...args, "test"));
  } else {
    // 此分支内event被收窄为MyEvent<"interactionCreate">,args类型为[number]
    client.on(event.name, (...args) => event.execute(...args, "test"));
  }
})

方案3:类型断言快速修复(适合临时场景,类型安全性弱)

如果不想调整现有代码结构,可以通过类型断言手动指定参数类型,跳过TS的类型校验:

Object.values(events).forEach((event) =>
  client[event.name === "ready" ? "once" : "on"](event.name, (...args: any[]) =>
    event.execute(...args as Parameters<typeof event.execute> extends [...infer P, string] ? P : never, "test")
  )
)

内容的提问来源于stack exchange,提问作者user16985789

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 18:21:52