https://github.com/samthor/typed-event-types
Typed events for EventTarget subclasses
https://github.com/samthor/typed-event-types
eventtarget types typescript
Last synced: 8 months ago
JSON representation
Typed events for EventTarget subclasses
- Host: GitHub
- URL: https://github.com/samthor/typed-event-types
- Owner: samthor
- License: apache-2.0
- Created: 2022-07-17T03:24:52.000Z (almost 4 years ago)
- Default Branch: main
- Last Pushed: 2022-07-18T00:29:47.000Z (almost 4 years ago)
- Last Synced: 2025-02-02T08:11:19.504Z (over 1 year ago)
- Topics: eventtarget, types, typescript
- Language: JavaScript
- Homepage:
- Size: 16.6 KB
- Stars: 12
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
This package provides TypeScript types that help you add typed events to your `EventTarget` subclasses.
## Usage
To create a subclass of `EventTarget` that has custom events:
```ts
import type { AddEvents } from 'typed-event-types';
interface WhateverEventMap {
whatever: CustomEvent;
}
class ObjectWithEvents extends (EventTarget as AddEvents) {}
const o = new ObjectWithEvents();
o.addEventListener('whatever', (e) => {
console.info(e.detail); // <-- TS now knows this is a number
});
```
This also works for Custom Elements (or anything else that _already has_ events), and keeps all the existing events:
```ts
class ElementWithMoreEvents extends (HTMLElement as AddEvents) {}
const e = new ElementWithMoreEvents();
e.addEventListener('whatever', (e) => {
console.info(e.detail); // <-- TS now knows this is a number
});
e.addEventListener('click', (e) => {
console.info(e.movementX); // <-- TS still knows this is a MouseEvent
});
```
Great!
You can also extend your types _again_, because that's the point:
```ts
class ExtendedEvenMoreEvents extends (ElementWithMoreEvents as AddEvents;
}>) {}
const ee = new ExtendedEvenMoreEvents();
ee.addEventListener('anotherEvent', (e) => {
console.info(e.detail); // <-- TS now knows this is a string
});
ee.addEventListener('whatever', (e) => {
console.info(e.detail); // <-- TS still knows this is a number
});
```
## Background
This has historically been a hard problem.
TypeScript internally solves this by adding/replacing the `addEventListener` call on all its internal interfaces whenever it needs to add more types.
But this isn't _additive_, you need to replace all the events every time.
For example, on the media elements, TypeScript does this:
```ts
interface HTMLMediaElementEventMap extends HTMLElementEventMap {
"encrypted": MediaEncryptedEvent;
"waitingforkey": Event;
}
interface HTMLMediaElement extends HTMLElement {
addEventListener(type: K, listener: (this: HTMLMediaElement, ev: HTMLMediaElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
}
```
…which is fine, but annoying for developers to do _every time_: you can't extend something easily as you need to know what set of events your parent had.
The approach in this package solves this by adding specific `addEventListener` etc calls which accept a type literal, which TypeScript happily hoists and merges with the other keys.
(TypeScript could use this approach internally, too—it would be a bit more verbose in the _generated_ types, but make the built-in types much easier to write.)
So how does this work?
Well… check out the source.
## Attributions
This internally includes `UnionToIntersection` [from here](https://fettblog.eu/typescript-union-to-intersection/).
Thanks, [@ddprrt](https://twitter.com/ddprrt).
## Contributions
### Unknown This
Because of the way the `addEventListener` etc calls are added, we don't know what `this` is at the time.
Listeners added this way should get the `this` of the `EventTarget`.
This is a limitation but _only if you're using_ `function() { ... }`, which, …you probably aren't, anymore.
So if you're writing inline handlers, use `() => { ... }`.
To be clear, this isn't _dangerous_: we just say that `this` is `EventTarget`, which is true, but not very useful.
TypeScript will complain at you.
This doesn't effect self-referential cases like this:
```ts
class ElementWithMoreEvents extends (HTMLElement as AddEvents) {
constructor() {
super();
this.addEventListener('whatever', this.unboundHandler);
}
unboundHandler() {
// this will be from the caller, and for events, it's set correctly
}
}
```
Maybe you can help fix… `this`? 👀
### Verbose Syntax
The syntax is pretty verbose.
I've tried to make it slightly nicer to work with a builder function—it doesn't do anything, just returns the argument but casts it—but it ends up being a bit unsafe.
Want to contribute?
[@-me](https://twitter.com/samthor) or you know, do GitHub things.
## Usage
Install via "typed-event-types".
But!
If you want to take this code and include it in a TS helper library—I'd love that!
It is a bit niche, as it specifically targets the DOM.
Just attribute me (Apache-2.0) or tell me where to contribute the PR.