TypeScript:如何基于lastEvent的类型窄化emit方法的参数类型?
示例代码
type Event<S extends string> = { name: S } const EventHandler = <S extends string, T extends Event<S>[]>(arr: [...T]) => { let lastEvent: T[number] = arr[0] return { set: (event: T[number]) => lastEvent = event, emit: (event: typeof lastEvent['name']) => { }, lastEvent } } const emitter = EventHandler([{ name: 'A' }, { name: 'B' }]) if(emitter.lastEvent.name === 'A') { // `emitter.lastEvent` 现在被推断为 `{ name: 'A' }` emitter.emit('B') // 可以编译,但我希望仅允许字符串 `'A'` }
问题描述
在这个简化示例中,我创建了带有lastEvent配置项的EventHandler对象,期望emit方法的参数能根据lastEvent的当前类型自动窄化限制。
但即便在条件判断里lastEvent的类型已经被窄化为{ name: 'A' },emit方法却依然接受'B'作为参数。请问有没有办法实现「emit参数随lastEvent类型同步窄化」的需求?最好不用自定义类型守卫。
编辑补充
我试过一个折中方案,把事件对象作为emit的第一个参数:
emit: (event: T[number], name: typeof event['name']) => { }
调用时写:
emitter.emit({ name: 'A' }, 'B') // 为什么这能通过编译?!
结果类型推断还是不生效,这让我很困惑——TypeScript为什么处理不了这种场景?
解决方案及解释
核心问题原因
TypeScript的类型窄化仅作用于值本身,而emit方法的参数类型是在EventHandler初始化时就确定的(即T[number]['name'],也就是'A' | 'B')。它无法追踪lastEvent这个内部可变状态的运行时类型变化,因此不会自动更新emit的参数约束。
实现需求的正确方式:用泛型绑定不可变状态
要让emit参数随lastEvent类型同步限制,需要让EventHandler返回的对象类型与lastEvent的具体类型强绑定,并且通过不可变更新的方式(每次set返回新实例)让TypeScript能追踪类型变化。
修改后的代码:
type Event<S extends string> = { name: S } const EventHandler = <E extends Event<string>>(initialEvent: E) => { let lastEvent: E = initialEvent; return { // set方法返回新的EventHandler,类型与新事件绑定 set: <NewE extends Event<string>>(event: NewE) => EventHandler(event), // emit参数类型直接绑定当前lastEvent的name类型 emit: (event: E['name']) => {}, lastEvent } } // 初始化时绑定初始事件类型 const emitter = EventHandler({ name: 'A' }); emitter.emit('A'); // 正常编译 emitter.emit('B'); // 报错:类型'"B"'的参数不能赋给类型'"A"'的参数 // 调用set切换事件后,新实例的emit自动适配新类型 const emitterB = emitter.set({ name: 'B' }); emitterB.emit('B'); // 正常编译 emitterB.emit('A'); // 报错
关于折中方案的疑问解答
你之前的折中方案之所以允许emitter.emit({ name: 'A' }, 'B')编译,是因为T[number]是联合类型{ name: 'A' } | { name: 'B' },TypeScript会将event推断为这个联合类型,对应的typeof event['name']就是'A' | 'B',所以'B'会被判定为合法参数。
要修复这个问题,需要给emit方法添加泛型参数,明确约束event的具体类型:
// 修改后的emit定义 emit: <E extends T[number]>(event: E, name: E['name']) => { }
此时调用时,TypeScript会根据传入的event推断出具体的E类型,进而强制name必须与event.name完全匹配:
emitter.emit({ name: 'A' }, 'B'); // 报错:类型'"B"'的参数不能赋给类型'"A"'的参数 emitter.emit({ name: 'A' }, 'A'); // 正常编译
内容的提问来源于stack exchange,提问作者Alexmedkex

