Repository: TypeScript
Issue: Add a generic parameter to EventTarget for mapping events
Labels: Suggestion,Awaiting More Feedback

# Suggestion


## 🔍 Search Terms

EventTarget, addEventListener, removeEventListener

## ✅ Viability Checklist

* [ ] This wouldn't be a breaking change in existing TypeScript/JavaScript code
* [x] This wouldn't change the runtime behavior of existing JavaScript code
* [x] This could be implemented without emitting different JS based on the types of the expressions
* [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
* [x] This feature would agree with the rest of [TypeScript's Design Goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals).


## ⭐ Suggestion

Currently we can extend `EventTarget` to hook any given class into the DOM events system:

```ts
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:

```ts
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:

```ts
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`:

```ts
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:

```ts
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:

```ts
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:

```ts
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 {}
```