Description
Suggestion
π Search Terms
EventTarget, addEventListener, removeEventListener
β Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
β Suggestion
Currently we can extend EventTarget
to hook any given class into the DOM events system:
class Foo extends EventTarget {}
const instance = new Foo();
instance.addEventListener('my-event', (ev) => { ... });
However, in the case of EventTarget
, it has an addEventListener
definition like so:
interface EventTarget {
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject | null,
options?: boolean | AddEventListenerOptions
): void;
dispatchEvent(event: Event): boolean;
removeEventListener(
type: string,
callback: EventListenerOrEventListenerObject | null,
options?: EventListenerOptions | boolean
): void;
}
declare var EventTarget: {
prototype: EventTarget;
new(): EventTarget;
};
Due to this, it seems impossible to strongly type the events of our class.
Remember how things like window
work:
addEventListener<K extends keyof WindowEventMap>(
type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void;
This means we have strongly typed event names and types, giving us good intellisense when we do window.addEventListener
.
So my suggestion is that we do similar for EventTarget
:
interface EventTarget<EventMap> {
addEventListener<K extends keyof EventMap>(
type: K,
listener: (this: any, ev: EventMap[K]) => any, // `this` has to be `any` i suppose since we don't know what it is at this point
options?: boolean | AddEventListenerOptions
): void;
}
We'd still want an overload which consumes string
but that would just fall back to Event
.
Also, I'm unaware of any work around, so if there is already a known one, please do tell.
We also can't override this as a workaround, for example:
class Foo extends EventTarget {
addEventListener<K extends keyof FooMap>(
type: K,
listener: (ev: FooMap[K]) => void,
options?: boolean | AddEventListenerOptions
): void;
// ...[implementation and other overloads here]
}
as this would be like doing:
type Handler = (ev: Event) => void;
const myHandler: Handler = (ev: CustomEvent) => void; // error since it needs to handle ANY `Event`
You could possibly hack around it with interfaces:
interface FooEventTarget extends EventTarget {
addEventListener<K extends keyof FooMap>(
type: K,
listener: (ev: FooMap[K]) => void,
options?: boolean | AddEventListenerOptions
): void;
addEventListener(
type: string,
callback: EventListenerOrEventListenerObject | null,
options?: EventListenerOptions | boolean
): void;
}
const eventTarget = EventTarget as {new(): FooEventTarget; prototype: FooEventTarget};
class MyClass extends eventTarget {}