question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add type to retrieve valid events of an EventTarget

See original GitHub issue

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.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:17
  • Comments:5 (1 by maintainers)

github_iconTop GitHub Comments

2reactions
jsejcksncommented, Jul 6, 2020

Also, EventTarget should be able to accept a type argument which is an EventMap, so that classes can extend EventTargetand the corresponding listeners will recognize the appropriate types and events.

Here’s an example:

type SpellCastEvent = CustomEvent<{
  id: number;
  effect: string;
  name: string;
}> & {
  type: 'spell_cast';
};

type SpellcasterEventMap = {
  spell_cast: SpellCastEvent;
};

class Spellcaster extends EventTarget<SpellcasterEventMap> {
  constructor() {
    super();
    // ...
  }
}

function spellEventHandler (event: SpellCastEvent) {
  console.log(`The ${event.detail.name} spell was cast.`);
}

const spellcaster = new Spellcaster();

spellcaster.addEventListener('spell_cast', spellEventHandler);

Right now, this causes an error when calling spellcaster.addEventListener(), and has to be addressed with a type assertion in the handler like so:

function spellEventHandler (event: Event) {
  const ev = (event as SpellCastEvent);
  console.log(`The ${ev.detail.name} spell was cast.`);
}

You can see the errors I’m describing in #39425.

1reaction
pushkinecommented, Oct 6, 2020

The first solution coming to my mind is related to #26591

type EventTypes<El extends EventTarget> = Parameters<El["addEventListener"]>[0];

Yet there’s also the fact that string | "custom_event_type" collapses to string I can’t really think of a good solution but in the mean time here’s my usual goto

type EventMap<T extends EventTarget> = T extends MediaQueryList
	? MediaQueryListEventMap
	: T extends Document
	? DocumentEventMap
	: T extends Window
	? WindowEventMap
	: HTMLElementEventMap;
type EventTypes<T extends EventTarget> = keyof EventMap<T> & string;
type EventValue<T extends EventTarget, K extends EventTypes<T>> = Extract<EventMap<T>[K], Event>;

function listen<T extends EventTarget, K extends EventTypes<T>>(
	element: T,
	type: K,
	listener: (this: T, ev: EventValue<T, K>) => void,
	opts: AddEventListenerOptions
) {
	element.addEventListener(type, listener, opts);
	return element.removeEventListener.bind(element, type, listener, opts);
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

EventTarget.addEventListener() - Web APIs | MDN
The method addEventListener() works by adding a function, or an object that implements EventListener , to the list of event listeners for the ......
Read more >
How to fix property not existing on EventTarget in TypeScript
This error occurs when you try to access a property on an event target in TypeScript. Property 'value' does not exist on type...
Read more >
Why is Event.target not Element in Typescript? - Stack Overflow
var tag = evt.target.tagName.toLowerCase();. While Event.target is of type EventTarget , it does ...
Read more >
How event binding works - Angular
The type of $event.target is only EventTarget in the template. In the getValue() method, the target is cast to an HTMLInputElement to allow ......
Read more >
target Event Property - W3Schools
Example. Using the event.target property together with the element.tagName property to find out which element triggered a specified event:.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found