TypeScript泛型封装addEventListener非法参数未报错问题
我正在尝试实现一个辅助函数,接收目标对象、事件类型、回调函数三个入参,函数内部调用目标对象的addEventListener方法传入其余参数,返回值为一个函数,调用该函数即可移除对应的事件监听器。
我希望该函数可适配HTMLElement、Document、Window等不同类型的对象,由于这些类型不存在公共父类,因此我采用泛型方案实现,代码如下:
interface EventSender<Type, ParameterType> { addEventListener(t: Type, listener: (evt: ParameterType) => unknown): void; removeEventListener(t: Type, listener: (evt: ParameterType) => unknown): void; } function myAddEventListener <Type, ParameterType, Sender extends EventSender<Type, ParameterType>> (o: Sender, t: Type, listener: (evt: ParameterType) => unknown) { o.addEventListener(t, listener); return () => o.removeEventListener(t, listener); }
当我按如下方式调用该函数时,TypeScript未抛出任何编译错误,这与我的预期不符:Document和Window的addEventListener方法本不接受number[]作为首个参数,按理该调用应当违反EventSender的类型约束,却可正常编译:
myAddEventListener<number[], string, Document>(document, [1, 2, 3], (_x: string) => undefined); // 正常编译 myAddEventListener<number[], string, Window>(window, [1, 2, 3], (_x: string) => undefined); // 正常编译
而我直接调用原生的addEventListener方法传入上述非法参数时,TypeScript会如期抛出编译错误:
document.addEventListener([1, 2, 3], (_x: string) => undefined); // 编译错误:No overload matches this call window.addEventListener([1, 2, 3], (_x: string) => undefined); // 编译错误:No overload matches this call
此外我自定义了Foo类,其addEventListener和removeEventListener的方法签名与Document完全一致,传入相同非法参数调用myAddEventListener时,TypeScript如期抛出类型不满足约束的编译错误:
type DocumentAddEventListenerParams = Parameters<typeof Document.prototype.addEventListener>; type DocumentAddEventListenerReturn = ReturnType<typeof Document.prototype.addEventListener>; type DocumentRemoveEventListenerParams = Parameters<typeof Document.prototype.removeEventListener>; type DocumentRemoveEventListenerReturn = ReturnType<typeof Document.prototype.removeEventListener>; class Foo { addEventListener(...params: DocumentAddEventListenerParams): DocumentAddEventListenerReturn { return; } removeEventListener(...params: DocumentRemoveEventListenerParams): DocumentRemoveEventListenerReturn { return; } } myAddEventListener<number[], string, Foo>(new Foo(), '[1, 2, 3]', (_x: string) => undefined); // 编译错误:Type 'Foo' does not satisfy the constraint 'EventSender<number[], string>'.
核心疑问:为什么传入Document、Window时非法调用可以正常编译,而传入自定义Foo类时会如期报错?按照预期,所有不满足泛型约束的调用都应当编译失败。
出现这个差异的核心原因有两点:
- TypeScript对
Document/Window/HTMLElement这类内置DOM类型做了特殊的类型检查豁免,这类类型的方法参数默认采用双变的宽松检查规则,且方法重载的兼容性判断逻辑比自定义类型宽松很多。
原生addEventListener在TS的内置类型定义里是多重重载方法,第一个参数是所有合法事件名的字符串字面量联合类型。当你手动指定泛型参数Sender为Document/Window时,TS不会严格校验它是否完全匹配EventSender<number[], string>的结构,会直接判定重载方法可以兼容传入的number[]类型参数,因此不会抛出约束错误。
你直接调用原生addEventListener传非法参数时报错,走的是另一套重载匹配逻辑:调用时TS会逐个比对所有重载签名,发现没有任何一个签名接受number[]作为第一个参数,就会抛出重载不匹配错误,这套逻辑和泛型约束检查的兼容性判断逻辑并不一致。 - 你自定义的
Foo类不属于TS内置的DOM特殊类型列表,会走严格的结构化类型检查。Foo的addEventListener是通过工具类型推导出来的单签名方法,检查时严格遵循函数参数逆变规则:number[]和事件名的字符串联合类型完全不兼容,因此会直接抛出类型不满足约束的错误。
不需要手动定义EventSender接口做约束,直接基于TS内置的EventTarget基类(所有支持事件监听的DOM类型都继承自该类)推导参数类型即可,无需手动传入泛型就能获得严格正确的类型检查:
function myAddEventListener<T extends EventTarget>( target: T, type: Parameters<T['addEventListener']>[0], listener: Parameters<T['addEventListener']>[1], options?: Parameters<T['addEventListener']>[2] ) { target.addEventListener(type, listener, options); return () => target.removeEventListener(type, listener, options); } // 非法调用会正确抛出编译错误 myAddEventListener(document, [1,2,3], (_x: string) => undefined); // 编译错误 myAddEventListener(window, [1,2,3], (_x: string) => undefined); // 编译错误 // 合法调用可自动推导事件对象类型 myAddEventListener(document, 'click', (e) => { console.log(e.clientX); // 自动推导为MouseEvent,类型提示正常 })
内容的提问来源于stack exchange,提问作者guy

