I'm trying to write an abstraction layer on the addEventListener
method, but am running into a typing issue. In the minimal reproduction below;
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);
});
TypeScript complains that the parameter type is incompatible with 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.
I know that the following (basic) event listener:
window.addEventListener('mousedown', (event: MouseEvent) => {
console.log(event.pageX);
});
works just fine, so I'm assuming something is wrong in typing the handler
parameter as EventListener
, but I can't figure out what it should be.. Most answers I came across seem specific to React, which is not relevant in my case.
Above codesnippet on: TypeScript Playground | CodeSandbox
The problem is that your addEventListener()
typing has no idea how the type
parameter relates to the subtype of Event
that handler
should accept. In the TypeScript standard library file lib.dom.d.ts
, individual EventTarget
types are given a very specific mapping from type
to handler
. For example, the Window
interface's signature for its addEventListener
method looks like this:
addEventListener<K extends keyof WindowEventMap>(
type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void;
where WindowEventMap
is defined here as a large mapping between type
names and Event
type. For the mousedown
key specifically, the property value type is MouseEvent
. Thus when you call
window.addEventListener('mousedown', (event: MouseEvent) => {
console.log(event.pageX);
});
everything works because K
is inferred to be "mousedown"
and thus the handler
parameter expects a MouseEvent
.
Without that sort of mapping information, the typings tend to look like type
is string
and listener
is (event: Event)=>void
. But, as you've seen, you can't simply pass a (event: MouseEvent)=>void
to a parameter expecting a (event: Event)=>void
, because it's not safe in general to do this. If you give me a function that only accepts MouseEvent
s, I can't use it as a function that accepts all Event
s. If I try, and the function you give me accesses something like the pageX
property, I might get a runtime error.
This sort of restriction on function arguments was added in TypeScript 2.6 as a --strictFunctionTypes
compiler option. If you turn that compiler option off your error should disappear, but I don't recommend you do that.
Instead, if you want to loosen the typing just for your addEventListener
function, you could make it generic in the Event
subtype, like this:
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);
}
Then your code will compile:
addEventListener(window, "mousedown", (event: MouseEvent) => {
console.log(event.pageX);
});
But remember: this typing is too loose to catch errors, such as this:
addEventListener(document.createElement("div"), "oopsie",
(event: FocusNavigationEvent) => { console.log(event.originLeft); }
);
If you were to try that directly on an HTMLDivElement
you'd get an error:
document.createElement("div").addEventListener("oopsie",
(event: FocusNavigationEvent) => { console.log(event.originLeft); }
); // error! oopsie is not acceptable
Which wouldn't be resolved until you fixed both the type
parameter
document.createElement("div").addEventListener("blur",
(event: FocusNavigationEvent) => { console.log(event.originLeft); }
); // error! FocusNavigationEvent is not a FocusEvent
And used the matching Event
subtype
document.createElement("div").addEventListener("blur",
(event: FocusEvent) => { console.log(event.relatedTarget); }
); // okay now
So, that's about as far as I can go here. You could try to make an enormous mapping from all known EventTarget
types to all the correct type
parameters for each target, and then to all the correct Event
subtypes for each EventTarget
/type
pair, and have a single generic addEventListener
function that works... but it would be comparable in size to the lib.dom.d.ts
file, and I'm not inclined to spend too much more time on this. If you need type safety you would probably be better off not using this particular abstraction. Otherwise, if you're okay with the compiler missing some mismatches, then the above typing could be a way to go.
Okay, hope that helps; good luck!