如何在 TypeScript 中包装的 addEventListener 调用中正确键入事件处理程序

Rij*_*ijk 6 javascript addeventlistener typescript typescript-typings

我正在尝试在该addEventListener方法上编写一个抽象层,但遇到了打字问题。在下面的最小复制中;

function addEventListener<T extends EventTarget>(
  element: T,
  type: string,
  handler: EventListener
) {
  element.addEventListener(type, handler);
}

addEventListener<Window>(window, "mousedown", (event: MouseEvent) => {
  console.log(event.pageX);
});

Run Code Online (Sandbox Code Playgroud)

TypeScript 抱怨参数类型与 MouseEvent 不兼容:

Argument of type '(event: MouseEvent) => void' is not assignable to parameter of type 'EventListener'.
  Types of parameters 'event' and 'evt' are incompatible.
    Type 'Event' is missing the following properties from type 'MouseEvent': altKey, button, buttons, clientX, and 20 more.
Run Code Online (Sandbox Code Playgroud)

我知道以下(基本)事件侦听器:

Argument of type '(event: MouseEvent) => void' is not assignable to parameter of type 'EventListener'.
  Types of parameters 'event' and 'evt' are incompatible.
    Type 'Event' is missing the following properties from type 'MouseEvent': altKey, button, buttons, clientX, and 20 more.
Run Code Online (Sandbox Code Playgroud)

工作得很好,所以我假设在将handler参数输入as 时出现问题EventListener,但我无法弄清楚它应该是什么..我遇到的大多数答案似乎特定于 React,这与我的情况无关。

上面的代码片段:TypeScript Playground | 代码沙盒

jca*_*alz 7

问题是,你的addEventListener()打字不知道该怎么办type参数涉及的亚型Eventhandler应该接受的。在 TypeScript 标准库文件中lib.dom.d.ts,各个EventTarget类型被赋予了一个非常具体的从type到 的映射handler。例如,Window接口的addEventListener方法签名如下所示

addEventListener<K extends keyof WindowEventMap>(
  type: K, 
  listener: (this: Window, ev: WindowEventMap[K]) => any, 
  options?: boolean | AddEventListenerOptions
): void;
Run Code Online (Sandbox Code Playgroud)

其中WindowEventMap这里定义为之间有大的映射type的名称和Event类型。mousedown具体来说,对于键,属性值类型是MouseEvent。因此当你打电话

window.addEventListener('mousedown', (event: MouseEvent) => {
  console.log(event.pageX);
});
Run Code Online (Sandbox Code Playgroud)

一切正常,因为K推断为是"mousedown",因此handler参数需要 a MouseEvent


如果没有那种映射信息,类型往往看起来像typeisstringlisteneris (event: Event)=>void。但是,正如您所见,您不能简单地将 a 传递(event: MouseEvent)=>void给期望 a 的参数(event: Event)=>void,因为这样做通常是不安全的。如果你给我一个只接受MouseEvents的函数,我就不能把它当作一个接受所有Events的函数。如果我尝试,并且您给我的函数访问类似pageX属性的内容,我可能会收到运行时错误。

这种对函数参数的限制是在 TypeScript 2.6 中作为--strictFunctionTypes编译器选项添加的。如果您关闭该编译器选项,您的错误应该会消失,但我不建议您这样做。

相反,如果您只想为您的addEventListener函数放宽类型,您可以在Event子类型中使其通用,如下所示:

function addEventListener<T extends EventTarget, E extends Event>(
  element: T, type: string, handler: (this: T, evt: E) => void) {
  element.addEventListener(type, handler as (evt: Event) => void);
}
Run Code Online (Sandbox Code Playgroud)

然后你的代码将编译:

addEventListener(window, "mousedown", (event: MouseEvent) => {
  console.log(event.pageX);
});
Run Code Online (Sandbox Code Playgroud)

但请记住:这种打字太松,无法捕捉错误,例如:

addEventListener(document.createElement("div"), "oopsie",
  (event: FocusNavigationEvent) => { console.log(event.originLeft); }
);
Run Code Online (Sandbox Code Playgroud)

如果你直接在 an 上尝试,HTMLDivElement你会得到一个错误:

document.createElement("div").addEventListener("oopsie",
  (event: FocusNavigationEvent) => { console.log(event.originLeft); }
); // error! oopsie is not acceptable
Run Code Online (Sandbox Code Playgroud)

在您修复这两个type参数之前,这不会得到解决

document.createElement("div").addEventListener("blur",
  (event: FocusNavigationEvent) => { console.log(event.originLeft); }
); // error! FocusNavigationEvent is not a FocusEvent
Run Code Online (Sandbox Code Playgroud)

并使用匹配的Event子类型

document.createElement("div").addEventListener("blur",
  (event: FocusEvent) => { console.log(event.relatedTarget); }
); // okay now
Run Code Online (Sandbox Code Playgroud)

所以,这就是我所能做到的。您可以尝试将所有已知EventTarget类型映射到type每个目标的所有正确参数,然后映射到Event每个EventTarget/type对的所有正确子类型,并具有一个有效的通用addEventListener函数......但它可以在lib.dom.d.ts文件的大小,我不想在这上面花太多时间。如果您需要类型安全,最好不要使用这个特定的抽象。否则,如果您对编译器遗漏了一些不匹配的情况感到满意,那么上述类型可能是一种可行的方法。


好的,希望有帮助;祝你好运!

游乐场链接