为何TypeScript无法从已知EventName推断对应Payload类型?
先看给定的类型定义:
export type ListenerToAll<Events> = <EventName extends keyof Events>(eventName: EventName, payload: Events[EventName]) => void
以下代码会触发TypeScript报错:
type CharacterEvents = { wait: {turns: number}, speak: {message: string} } const liToAll: ListenerToAll<CharacterEvents> = (eventName, payload) => { if (eventName == 'wait') { // 尝试对eventName做类型收窄 // TS报错:类型'{ turns: number; } | { message: string; }'上不存在属性'turns' console.log(payload.turns) } }
问题原因
ListenerToAll<CharacterEvents>是一个泛型函数类型,它要求函数能支持任意EventName extends keyof CharacterEvents的调用场景——也就是说,调用者可以传入"wait"+对应payload,也可以传入"speak"+对应payload。
但当你把一个普通非泛型函数赋值给这个类型时,TypeScript会将eventName推断为"wait" | "speak"的联合类型,payload推断为{turns: number} | {message: string}的联合类型。此时两个参数是独立的联合类型,而非绑定在一起的关联元组。TypeScript无法通过收窄eventName的类型来联动收窄payload,因为当前函数签名没有表达“eventName和payload必须成对匹配”的约束。
简单来说,泛型函数类型的要求是函数本身能处理任意指定的事件类型,但你写的实现函数是一个处理联合类型参数的普通函数,两者的类型逻辑不匹配,导致类型收窄无法联动。
解决方案
方案1:让实现函数也保持泛型
和定义的泛型函数类型匹配,将实现函数声明为泛型函数:
const liToAll: ListenerToAll<CharacterEvents> = <EventName extends keyof CharacterEvents>(eventName: EventName, payload: CharacterEvents[EventName]) => { if (eventName === 'wait') { console.log(payload.turns); // 正确推断payload类型为{turns: number} } else { console.log(payload.message); // 自动推断payload类型为{message: string} } };
方案2:修改类型定义为联合函数签名
将ListenerToAll改为基于事件的联合函数类型,让TypeScript能识别参数的关联关系:
// 修改后的类型定义 export type ListenerToAll<Events> = { [K in keyof Events]: (eventName: K, payload: Events[K]) => void }[keyof Events]; // 原实现代码可正常工作 const liToAll: ListenerToAll<CharacterEvents> = (eventName, payload) => { if (eventName == 'wait') { console.log(payload.turns); // 无报错,正确推断类型 } };
这个修改后的类型会将ListenerToAll<CharacterEvents>展开为两个函数签名的联合:((eventName: "wait", payload: {turns: number}) => void) | ((eventName: "speak", payload: {message: string}) => void)。此时TypeScript会将其视为判别式联合,通过eventName的值就能自动联动推断对应payload的类型。
内容的提问来源于stack exchange,提问作者UXK

