0

I am writing a method in my library that can register a bunch of user-provided event listeners. Here is a simplified example:

function registerEventListeners(events: Record<string, EventListenerOrEventListenerObject>): void {
  for (const [name, handler] of Object.entries(events)) {
    window.addEventListener(name, handler)
  }
}

const customEvents = {
  keydown: (e: KeyboardEvent) => {
    if (e.key === "Enter") {
      // ...
    }
  }
}

registerEventListeners(customEvents)

TypeScript is giving me this error at the call site of registerEventListeners...

~      1. Argument of type '{ keydown: (e: KeyboardEvent) => void; }' is not assignable to parameter of type 'Record<string, EventListenerOrEventListenerObject>'.
~           Property 'keydown' is incompatible with index signature.
~             Type '(e: KeyboardEvent) => void' is not assignable to type 'EventListenerOrEventListenerObject'.
~               Type '(e: KeyboardEvent) => void' is not assignable to type 'EventListener'.
~                 Types of parameters 'e' and 'evt' are incompatible.
~                   Type 'Event' is missing the following properties from type 'KeyboardEvent': altKey, charCode, code, ctrlKey, and 17 more.

The EventListenerOrEventListenerObject generic seems to be unable to guarantee that my keydown event handler will receive the correct event type. So what is the correct way to type this code?

1
  • This is because an EventListener expects the parameter Event, while your function expects a KeyboardEvent. Because you have typed the events parameter as a record of event listeners that take any events, you have no safety that another kind of Event will be passed to your keydown handler. Commented Sep 1, 2022 at 18:35

1 Answer 1

1

As my comment describes, you are doing this:

(event: Event) => void;         // defined type
        |                       // lying to the given type
        V                       // by passing any event to it
(event: KeyboardEvent) => void; // given type

You can however use this compromise, sacrificing a line for //@ts-ignore, but getting the expected behavior for the end-user. Essentially you are now expecting a map of event names to the correct event type, instead of any string to the generic Event type.

function registerEventListeners(events: Partial<{
    [K in keyof WindowEventMap]: (event: WindowEventMap[K]) => void;
}>): void {
  for (const [name, handler] of Object.entries(events)) {
    //@ts-ignore
    window.addEventListener(name, handler);
  }
}

const customEvents = {
  keydown: (e: KeyboardEvent) => {
    if (e.key === "Enter") {
      // ...
    }
  }
}

registerEventListeners(customEvents)

Playground

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.