Javascript : typer et documenter événements et écouteurs d'événements

Petit topo sur la manière de documenter la gestion des événements dans des objets javascript, à la fois pour celleux qui codent et celleux qui utilisent le code...

Introduction

Imaginons une classe MyObject qui émet un événement action quand on appelle sa méthode doAction ; quelque chose comme ça (le code effectuant les traitements est volontairement omis pour se concentrer sur la gestion des événements) :

class MyObject extends EventTarget {
    doAction(name) {
        this.dispatchEvent(new CustomEvent('action', {detail:name}));
    }
}

On peut dés lors se mettre à l'écoute de ces événement sur une instance de la classe :

const instance = new MyObject();
instance.addEventListener('action', e => {});

La question est : comment documente-t-on la classe MyObject pour expliquer qu'elle peut émettre cet événement, et comment on caractérise les écouteurs d'événements pour savoir que l'objet reçu en paramètre est un CustomEvent ?

Surcharge de l'interface EventTarget

Une idée trouvée sur les internets (StackOverflow) et que j'ai trouvé un moment séduisante, c'est de surcharger les apis de l'interface EventTarget pour les caractériser plus finement. Cela pourrait donner quelque chose comme :

class MyObject extends EventTarget {
    /**
     * @param {'action'} type
     * @param {(e:CustomEvent<string>) => void} listener
     */
    addEventListener(type, listener) {
        super.addEventListener(type, listener);
    }
    
    /**
     * @param {CustomEvent<string>} event 
     * @returns 
     */
    dispatchEvent(event) {
        return super.dispatchEvent(event)
    }
    
    doAction(name) {
        this.dispatchEvent(new CustomEvent('action', {detail:name}));
    }
}

La technique est intéressante car elle fournit une bonne autocomplétion quand on veut écouter l'événement et elle renforce l'écriture du code puisque l'appel au dispatchEvent sera marqué en erreur si on ne passe pas le bon objet.

Elle a cependant deux gros défauts selon moi :

  1. Surcharger une API pour documenter, je ne trouve pas ça très bon !
  2. Elle n'est pertinente que si l'objet n'émet que le même type d'événement.

Documenter l'événement et l'écouteur

La technique que j'utilise le plus souvent consiste simplement à documenter l'événement et l'écouteur à l'aide de type dédié :

/** @typedef {CustomEvent<string>} MyObjectEvent */

class MyObject extends EventTarget {
    /**
     * {@link MyObjectEvent} fired when an action is performed and
     *  where detail is the action name.
     */
    static EVENT_ACTION = 'action';
    
    /**
     * @param {string} name 
     * @fires {@link MyObject.EVENT_ACTION} event.
     */
    doAction(name) {
        this.dispatchEvent(new CustomEvent('action', {detail: name}));
    }
}

Et son usage :

const instance = new MyObject();
instance.addEventListener(
    MyObject.EVENT_ACTION,
    (/**@type {MyObjectEvent}*/e) => {}
);

Cette architecture a l'énorme avantage de ne rien imposer dans l'écriture du code, si ce n'est la documentation.

Apis dédiés aux événements

Enfin, une autre solution consiste à créer des APIS spécifiques à chaque événement, quelque chose comme :

/** @typedef {CustomEvent<string>} MyObjectEvent */

class MyObject extends EventTarget {
    /**
     * @param {string} name 
     * @param {string} detail
     * @protected
     */
    dispatchMyObjectEvent(name, detail) {
        this.dispatchEvent(new CustomEvent(name, {detail}));
    }
    
    /**
     * @param {string} value 
     * @fires 'action' event.
     * @see addActionListener
     */
    doAction(name) {
        this.dispatchMyObjectEvent('action', name);
    }

    /**
     * {@link MyObjectEvent} fired when an action is performed and
     *  where detail is the action name.
     * 
     * @param {(e:MyObjectEvent) => boolean} listener 
     */
    addActionListener(listener) {
        this.addEventListener('action', listener);
    }

    removeActionListener(listener) {
        this.removeEventListener('action', listener);
    }
}

Un peu lourd, surtout quand l'objet émet de nombreux types d'événements, mais elle renforce considérablement la solidité du code.