Generically typed (yet specific to Event) custom useEvent hook in react typescript

Viewed 196

I'm writing a generic useEvent hook, which basically a wrapper on top of add(remove)EventListener is useEffect. I have the below code, which throws an error

const useEvent = (
  eventName: string,
  cb: (e: HTMLElementEventMap[keyof HTMLElementEventMap]) => void,
  options?: {
    element?: EventTarget;
    eventOptions?: EventListenerOptions;
    dependencies?: [];
  }
) => {
  const node = options?.element || document;
  useEffect(() => {
    node.addEventListener(eventName, cb);
    return () => {
      node.removeEventListener(eventName, cb);
    };
  }, [eventName, node, cb, options?.eventOptions, ...(options?.dependencies ?? [])]);
};
export default useEvent;

Usage,

const onKeydown = (e: KeyboardEvent) => {
     if (e.key === 'Escape') //do something;
}
useEvent('keydown', onKeydown, { element }); //Shows below error here on onKeydown

While using it, shows the following error on onKeydown in hook call

Argument of type '(e: KeyboardEvent) => void' is not assignable to parameter of type '(e: Event | ClipboardEvent | UIEvent | AnimationEvent | MouseEvent | InputEvent | FocusEvent | ... 11 more ... | WheelEvent) => void'.
  Types of parameters 'e' and 'e' are incompatible.
    Type 'Event | ClipboardEvent | UIEvent | AnimationEvent | MouseEvent | InputEvent | FocusEvent | ... 11 more ... | WheelEvent' is not assignable to type 'KeyboardEvent'.
      Type 'Event' is missing the following properties from type 'KeyboardEvent': altKey, charCode, code, ctrlKey, and 17 more.ts(2345)

Other approach which I tried where I'm expecting Eventtype as param:

const useEvent = <T extends Event>(
  eventName: keyof HTMLElementEventMap,
  handler: (e: T) => void,
  options: {
    element?: EventTarget;
    eventOptions?: EventListenerOptions;
    dependencies?: [];
  }
) => {
  const node = options?.element || document;
  useEffect(() => {
    //Shows below error here on handler
    node.addEventListener(eventName, handler, options?.eventOptions); 
    return () => {
      //Shows below error here on handler
      node.removeEventListener(eventName, handler);
    };
  }, [eventName, node, cb, options?.eventOptions, ...(options?.dependencies ?? [])]);
};

Usage:

useEvent<KeyboardEvent>('keydown', onKeydown);

Error I got:

Argument of type '(e: T) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject | null'.
  Type '(e: T) => void' is not assignable to type 'EventListener'.
    Types of parameters 'e' and 'evt' are incompatible.
      Type 'Event' is not assignable to type 'T'.
        'Event' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Event'.ts(2345)

I'm unsure of fixes for both the cases and looking for some help! Thanks in advance.

2 Answers

Associate eventName with the type of e with a generic:

const useEvent = <K extends keyof HTMLElementEventMap>(
  eventName: K,
  handler: (e: HTMLElementEventMap[K]) => void,
  options: {
    element?: EventTarget;
    eventOptions?: EventListenerOptions;
    dependencies?: []; // maybe unknown[] instead of only empty array?
  }
) => ...

I'm able to fix the code using as EventListenerorEventListenerObject for callback.

const useEvent = <T extends Event>(
  eventName: keyof HTMLElementEventMap,
  handler: (e: T) => void,
  options: {
    element?: EventTarget;
    eventOptions?: EventListenerOptions;
    dependencies?: [];
  }
) => {
  const node = options?.element || document;
  useEffect(() => {
    //Fix - handler as EventListenerorEventListenerObject
    node.addEventListener(eventName, handler as EventListenerorEventListenerObject, options?.eventOptions); 
    return () => {
      node.removeEventListener(eventName, handler as EventListenerorEventListenerObject);
    };
  }, []);
};
Related