FE / ts / js

Notes on TypeScript and CustomEvent

Quick notes on using CustomEvent with TypeScipt@5.3.3.

The Issue: No overload matches this call…

In case you tried to use a CustomEvent with TypeScript, chances are you experienced the No overload matches this call. error.

No overload matches this call.
  Overload 1 of 2, '(type: keyof DocumentEventMap, listener: (this: Document, ev: Event | UIEvent | AnimationEvent | MouseEvent | InputEvent | ... 13 more ... | WheelEvent) => any, options?: boolean | ... 1 more ... | undefined): void', gave the following error.
    Argument of type '"custom:event:name"' is not assignable to parameter of type 'keyof DocumentEventMap'.
  Overload 2 of 2, '(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void', gave the following error.
    Argument of type '(e: CustomEvent<string>) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'.
      Type '(e: CustomEvent<string>) => void' is not assignable to type 'EventListener'.
        Types of parameters 'e' and 'evt' are incompatible.
          Type 'Event' is missing the following properties from type 'CustomEvent<string>': detail, initCustomEvent(2769)

The error happens because in dom.generated.d.ts we have the EventListenerOrEventListenerObject which doesn’t support CustomEvents.

// https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L8150
interface EventListener {
    (evt: Event): void;
}

interface EventListenerObject {
    handleEvent(object: Event): void;
}

type EventListenerOrEventListenerObject = EventListener | EventListenerObject;

// https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L8199
addEventListener<K extends keyof AbortSignalEventMap>(type: K, listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;

The Quick and Dirty

// Register event 
const callback = (e: CustomEvent<string>) => console.log(e.detail);
document.addEventListener("custom:event:name", callback as EventListener);

// Alternative syntax inline
document.addEventListener("custom:event:name", ((e: CustomEvent<string>) => console.log(e.detail)) as EventListener);

// Fire event
const ev = new CustomEvent("custom:event:name", {detail: "yadda"});
document.dispatchEvent(ev);

The trick is in casting the callback in a EventListener even if we’re using CustomEvent instead of Event.

Extending EventTarget

As alternative, you can extend EventTarget which is the object that expose addEventListener, removeEventListener, and dispatchEvent.

The issue with this approach is that you can’t leverage document global scope, which possibly was the reason you tried to used it in the first place.

interface YourEventMap {
    "yourEvent": CustomEvent
}

interface YaddaEventTarget extends EventTarget  {
  addEventListener<K extends keyof YourEventMap>(event: K, listener: ((this: Yadda, ev: YourEventMap[K]) => any) | null, options?: AddEventListenerOptions | boolean): void;
  addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void;
}

class YaddaEventTarget extends EventTarget {
}

const instance = new YaddaEventTarget();

instance.addEventListener("yourEvent", (event: CustomEvent) => console.log(event));
instance.dispatchEvent(new CustomEvent("yourEvent", {detail: "bla"}));

Current status of the issue

As per Feb 2024, I see the issue on github getting bigger and bigger, with the discussion about the fix in stall so i guess that for a while this will stay relevant.