Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a generic parameter to EventTarget for mapping events #43477

Open
4 of 5 tasks
43081j opened this issue Apr 1, 2021 · 4 comments
Open
4 of 5 tasks

Add a generic parameter to EventTarget for mapping events #43477

43081j opened this issue Apr 1, 2021 · 4 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@43081j
Copy link

43081j commented Apr 1, 2021

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 {}
@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Apr 1, 2021
@masx200
Copy link

masx200 commented Jul 9, 2022

Why has this problem not been solved after so long?

@DerZade
Copy link

DerZade commented Oct 25, 2022

Why has this problem not been solved after so long?

Good question πŸ€”


I've wrote a small utility a couple of months ago for my work and used it in multiple projects since. I just released it as a npm / deno package:

Links:

Works with all kind of events as a drop in replacement of EventTarget and adds basically no bundle size.

@Symbitic
Copy link

I second this. EventTarget is such a core part of the web that it shouldn't take hacks to declare the typing for events. Especially for implementing pollyfills or Node implementations of web standards, this can be a real pain.

Besides making EventTarget generic, the only other thing I can think of is a new keyword like extendmethod for declaring methods that are specific instances of an existing super method (i.e. are compatible with the existing function) and merely provide type information about a specific subset.

For example:

export class MyTimer extends EventTarget {
  extendmethod addEventListener(
    type: "timestopped",
    listener: (this: this, ev: TimeEvent) => any,
    options?: boolean
  ): void;
}

This "timestopped" declaration is compatible with EventTarget.addEventListener that takes a string and a callback that accepts a class that extends Event. It doesn't need to call super, so no implementation is needed.

Some better words than "extendmethod" off the top of my head: "specific" "extra" "declare"

@43081j
Copy link
Author

43081j commented May 3, 2024

well i opened a draft at least in microsoft/TypeScript-DOM-lib-generator#1712

we shall see if its a feasible change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants