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
相关产品推荐
相关产品推荐

