microsoft / microsoft/TypeScript
Add type to retrieve valid events of an EventTarget
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 111k
- Forks
- 14.3k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 132
Description
Search Terms
EventListener, EventMap, EventTarget
Suggestion
There should a type to retrieve valid event names and listeners for a given EventTarget. Window has the following method:
addEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
Every EventTarget should have its own corresponding EventMap (even if no special event types are available for a given target, thus, resulting in an empty object). The EventMap of a given EventTarget could be retrieved as follows:
type WindowEventMap = EventMap<Window>;
type DocumentEventMap = EventMap<Document>;
type AudioNodeEventMap = EventMap<AudioNode>; // Empty interface
// ...
Use Cases
I tried creating a function which encapsulates an event for easier lifecycle management with React Hooks:
export function managedEventListener<T extends EventTarget, K extends string>(
target: T,
type: K,
callback: EventListener,
options?: AddEventListenerOptions,
) {
target.addEventListener(type, callback, options);
return () => {
target.removeEventListener(type, callback, options);
};
}
Unfortunately, EventListener gives no proper IntelliSense and I had to use the as EventListener syntax like below, as suggested in #28357:
useEffect(
() =>
managedEventListener(window, 'deviceorientation', ((
event: DeviceOrientationEvent,
) => {
setOrientation(event);
}) as EventListener),
[],
);
My goal was to simplify the syntax to the following, with proper type inference:
useEffect(
() =>
managedEventListener(window, 'deviceorientation', event => {
setOrientation(event);
}),
[],
);
Using conditional types, I was able to achieve the syntax above by replacing EventListener with a specialized EventListenerCallback type which extracts values from the 2 most commonly used event maps, namely WindowEventMap and DocumentEventMap:
type ExtractFrom<T, K> = K extends keyof T ? T[K] : never;
export type EventListenerCallback<T, K> = T extends Window
? (event: ExtractFrom<WindowEventMap, K>) => any
: (T extends Document
? (event: ExtractFrom<DocumentEventMap, K>) => any
: EventListener);
The new code for managedEventListener was born:
export function managedEventListener<T extends EventTarget, K extends string>(
target: T,
type: K,
callback: EventListenerCallback<T, K>,
options?: AddEventListenerOptions,
) {
target.addEventListener(type, callback, options);
return () => {
target.removeEventListener(type, callback, options);
};
}
Examples
The code above could be greatly simplified by introducing the aforementioned EventMap<EventTarget> type:
export function managedEventListener<T extends EventTarget, K extends keyof EventMap<T>>(
target: T,
type: K,
callback: (this: T, ev: EventMap<T>[K]) => any,
options?: AddEventListenerOptions,
) {
target.addEventListener(type, callback, options);
return () => {
target.removeEventListener(type, callback, options);
};
}
Checklist
My suggestion meets these guidelines:
- 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, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing the existing EventTarget event-listener types, including WindowEventMap and DocumentEventMap, and compare them with the proposed EventMap examples. Determine whether each EventTarget can expose a corresponding event map and how generic event names and listener parameters should be inferred. Done means the requested type supports the shown managedEventListener usage without casts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100