1

I'm updating my shorter-js codebase with proper JSDoc to generate TypeScript definitions but I'm unable to narrow down this one.

I have the on() snippet which makes use of native Element.addEventListener, so far so good, the problem is when using TouchEvent as a parameter for an actual handler, TypeScript throws a 4 line error, see below snippet.

/**
 * @param {HTMLElement | Element | Document} element event.target
 * @param {string} eventName event.type
 * @param {EventListener} handler callback
 * @param {EventListenerOptions | boolean | undefined} options other event options
 */
function on(element, eventName, handler, options) {
  const ops = options || false;
  element.addEventListener(eventName, handler, ops);
}

/**
 * test handler
 * @type {EventListener}
 * @param {TouchEvent} e
 */
function touchdownHandler(e){
  console.log(e.touches)
}

// test invocation
on(document.body, 'touchdown', touchdownHandler);
body {height: 100%}

The on(document.body, 'touchdown', touchdownHandler) invocation throws this 4 line error:

Argument of type '(e: TouchEvent) => void' is not assignable to parameter of type 'EventListener'.
  Type '(e: TouchEvent) => void' is not assignable to type 'EventListener'.
    Types of parameters 'e' and 'evt' are incompatible.
      Type 'Event' is missing the following properties from type 'TouchEvent': altKey, changedTouches, ctrlKey, metaKey, and 7 more.

In fact I get the exact same when using document.body.addEventListener(...) invocation. So I've tried various definitions within my index.d.ts, but nothing to resolve this.

To my knowledge I think I need to define something in my index.d.ts, then use it in the JSDoc for the touchdownHandler.

Any suggestions?

thednp
  • 4,401
  • 4
  • 33
  • 45

2 Answers2

3

Problem here is that you need to provide addEventListener with actual type of event, so it would map handler to accept this particular kind of event. dom.d.ts declaration (DOM Library) contain event maps for this particular usage. Your goal is to ensure that eventName is mapped to event type of handler.

While in TS we can use param types so it can be done without generics, you can't do same with JSDoc, so we have to introduce template variable for Event Type:

/**
 * @template {keyof HTMLElementEventMap} T
 * @param {HTMLElement} element
 * @param {T} eventName
*/

There are other event maps, but in most cases this will be enough. If not - check dom.d.ts source code.

Next we need to type out handler:

/**
 * @template {keyof HTMLElementEventMap} T
 * @param {HTMLElement} element
 * @param {T} eventName
 * @param {(event: HTMLElementEventMap[T]) => void} handler
 */

In this case, if T will be 'click', we will get event in handler as HTMLElementEventMap['click'], which is MouseEvent.

And last - options. In JSDoc you can mark parameter as optional instead of specifying undefined:

/**
 * @template {keyof HTMLElementEventMap} T
 * @param {HTMLElement} element
 * @param {T} eventName
 * @param {(event: HTMLElementEventMap[T]) => void} handler
 * @param {EventListenerOptions | boolean} [options]
 */

This will work as you expect.

Full Code:

/**
 * @template {keyof HTMLElementEventMap} T
 * @param {HTMLElement} element
 * @param {T} eventName
 * @param {(event: HTMLElementEventMap[T]) => void} handler
 * @param {EventListenerOptions | boolean} [options]
 */
function on(element, eventName,  handler, options) {
  const ops = options || false;
  element.addEventListener(eventName, handler, ops);
}

/**
 * test handler
 * @param {TouchEvent} e
 */
function touchdownHandler(e) {
  console.log(e.touches)
}

// test invocation
on(document.body, 'touchend', touchdownHandler);

Sandbox

To declare this in you have to use Generics:

declare function on<T extends keyof HTMLElementEventMap>(element: HTMLElement, eventName: T, handler: (event: HTMLElementEventMap[T]) => void, options?: EventListenerOptions | boolean): void;
Mr. Hedgehog
  • 2,644
  • 1
  • 14
  • 18
  • Thank you for providing an answer, can you also explain how to write the `@template` part in my `index.d.ts` as well? Thanks again! – thednp Jan 12 '22 at 10:31
  • I ask that because I will need `on()` to be used for `DocumentEventMap` as well, so I need to define it perhaps broader. I also have an `off()` and a `one()` variant as well, it won't make sense to define the template over and over again for each file. – thednp Jan 12 '22 at 10:36
  • In typescript it would be [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html). And `HTMLElementEventMap` already pretty broad since its definition is ` interface HTMLElementEventMap extends ElementEventMap, DocumentAndElementEventHandlersEventMap, GlobalEventHandlersEventMap {}`, but I'll update my answer to include it. – Mr. Hedgehog Jan 12 '22 at 10:38
  • While your sandbox example is working perfect, I believe would need to have separate definitions `declare type AddorRemoveEventListener`, then `declare type EventHandler` so I could somehow reuse/integrate with the `one()` utility. Thanks again. – thednp Jan 12 '22 at 12:07
  • Also this won't work without errors with `on(document, 'event', callback)`. – thednp Jan 12 '22 at 12:21
  • I really have no idea how to properly do this with JSDoc, but I have solution for ambient declaration. If this is good for you, I'll add it to my answer – Mr. Hedgehog Jan 12 '22 at 13:55
  • I made some experimentation with your code samples, it seems I can declare multiple `@template` within the same JSDoc definition block. I defined for each `Document` | `Element` | `HTMLElement` | `Window` and everything works as I originally expected. Right now I'm trying to convert it all to `index.d.ts` declaration. But your experienced input might be more valuable for my learning experience, so please go ahead. – thednp Jan 12 '22 at 14:09
  • If it works in TS playground, you can get generated declaration from sidebar (d.ts tab) – Mr. Hedgehog Jan 12 '22 at 14:11
  • Thank you again. I will mark your answer as THE answer, it resolves my initial question. [Here's](https://codesandbox.io/s/ujj7g) what I ended up with, IDK how to solve the new issue, currently tinkering with it. – thednp Jan 12 '22 at 14:22
1

Because I already marked an answer, I will provide my solution as well, which is much more simple than I thought: make use of the EventListenerObject with its handleEvent method:

/**
 * Add eventListener to an `Element` | `HTMLElement` | `Document` | `Window` target.
 *
 * @param {HTMLElement | Element | Document | Window} element event.target
 * @param {string} eventName event.type
 * @param {EventListenerObject['handleEvent']} handler callback
 * @param {(EventListenerOptions | boolean)=} options other event options
 */
function on(element, eventName, handler, options) {
  const ops = options || false;
  element.addEventListener(eventName, handler, ops);
}

Everything works as expected now.

on(document, 'load', (e) => console.log(e), false)

on(window, 'resize', (e) => console.log(e.type), false)

/**
 * test handler
 * @type {(event: Event) => void}
 * @param {TouchEvent} e
 */
 function touchdownHandler(e) {
  console.log(e.touches)
}

const div = document.createElement('div')
on(div, 'touchstart', touchdownHandler, false)

const main = document.createElement('main')
on(main, 'touchstart', touchdownHandler, false)
Dharman
  • 30,962
  • 25
  • 85
  • 135
thednp
  • 4,401
  • 4
  • 33
  • 45