TypeScript函数联合类型参数交叉问题的解决方法
实现类型安全的事件回调类型
目标结构
需要定义满足如下要求的函数类型:
(eventName: EventNames, ...params: [ParamsTypeForEventName]) => void
核心规则:传入的eventName可唯一确定对应事件的参数类型,已有事件名到参数元组的映射类型结构:
type T1 = { // 键为事件名,值为该事件对应的参数类型元组 [eventName: string]: any[] };
原有实现的问题
原有写法通过映射生成函数联合类型,代码如下:
const names = Object.freeze({ n1: 'n1', n2: 'n2' }); type T1 = { [names.n1]: [arg0: number], [names.n2]: [arg0: string, arg1: boolean] }; type ValueOf<T> = T[keyof T]; type ObjectWithFunc<T extends {[name: string]: any[]}> = { func: ValueOf<{ [Name in keyof T]: ((name:Name, ...args:T[Name])=>void) }> } let objectWithFunc:ObjectWithFunc<T1>; objectWithFunc = { func: ()=>{} }; objectWithFunc.func('n1',10); type ExtractedType = typeof objectWithFunc.func;
虽然悬停查看ExtractedType显示为两个函数的联合:
type ExtractedType = ((name: "n1", arg0: number) => void) | ((name: "n2", arg0: string, arg1: boolean) => void)
但TypeScript对函数联合做类型检查时,会对参数执行逆变合并,即参数取交叉类型,最终func的实际检查类型为:
(property) func: (name: never, arg0: never, arg1: boolean) => void
导致合法调用也会报类型错误。
正确实现
不要使用函数联合类型,改为定义泛型函数,调用时根据传入的事件名动态匹配对应参数类型,即可避免参数交叉问题:
const names = Object.freeze({ n1: 'n1', n2: 'n2' }); type T1 = { [names.n1]: [arg0: number], [names.n2]: [arg0: string, arg1: boolean] }; type ObjectWithFunc<T extends {[name: string]: any[]}> = { func: <EventName extends keyof T>( eventName: EventName, ...params: T[EventName] ) => void } let objectWithFunc:ObjectWithFunc<T1>; objectWithFunc = { func: ()=>{} }; // 类型校验符合预期 objectWithFunc.func('n1', 10); // 校验通过 objectWithFunc.func('n2', 'abc', true); // 校验通过 objectWithFunc.func('n1', '非法参数'); // 校验报错,参数类型不匹配 objectWithFunc.func('n2', 123, false); // 校验报错,第一个参数类型不匹配
实现原理:泛型参数EventName会在函数调用时根据传入的eventName自动推导为具体的事件名字面量类型,再直接从映射类型T中取对应的参数元组类型,整个过程不会生成函数联合,自然不会触发参数逆变交叉的逻辑,完全实现事件名和参数类型的绑定。
内容的提问来源于stack exchange,提问作者kharon4
相关产品推荐
相关产品推荐

