1

I'm trying to create a re-usable custom hook that enables a user close the menu when they either click off the node or press escape.

I've never used Typescript before and I want to destructure the event object rather than use it directly, e.g. event.type, and create type declarations for the object keys.

import * as React from "react";

interface IProps {
  type: React.MouseEvent<string> | React.KeyboardEvent<string>;
  target: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>;
  key?: React.KeyboardEvent<string>;
}

/**
 * Allows you to close a menu when user clicks outside of the nodes family tree.
 */
function useCloseMenu(
  ref: React.MutableRefObject<any>,
  isMenuOpen: boolean,
  callback: () => void
): void {
  React.useEffect(() => {
    function handleUserEvent({ type, target, key = null }: IProps): void {
      switch (type) {
        case "click":
          if (!ref.current?.contains(target) && isMenuOpen) {
            callback();
          }

        case "keydown":
          // "Esc" is an IE/Edge specific value
          if (key === ("Escape" || "Esc") && isMenuOpen) {
            callback();
          }
      }
    }

    document.addEventListener("click", handleUserEvent);
    document.addEventListener("keydown", handleUserEvent);
    return () => {
      document.removeEventListener("click", handleUserEvent);
      document.addEventListener("keydown", handleUserEvent);
    };
  }, [isMenuOpen]);
}

export default useCloseMenu;

use-close-menu.tsx:21:14 - error TS2678: Type 'string' is not comparable to type 'MouseEvent<string, MouseEvent> | KeyboardEvent<string>'.

21         case "click":
                ~~~~~~~

use-close-menu.tsx:26:14 - error TS2678: Type 'string' is not comparable to type 'MouseEvent<string, MouseEvent> | KeyboardEvent<string>'.

26         case "keydown":
                ~~~~~~~~~

use-close-menu.tsx:28:15 - error TS2367: This condition will always return 'false' since the types 'KeyboardEvent<string>' and 'string' have no overlap.

28           if (key === ("Escape" || "Esc") && isMenuOpen) {
                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~

use-close-menu.tsx:34:40 - error TS2769: No overload matches this call.
  Overload 1 of 2, '(type: "click", listener: (this: Document, ev: MouseEvent) => any, options?: boolean | AddEventListenerOptions): void', gave the following error.
    Argument of type '({ type, target, key }: IProps) => void' is not assignable to parameter of type '(this: Document, ev: MouseEvent) => any'.
      Types of parameters '__0' and 'ev' are incompatible.
        Type 'MouseEvent' is not assignable to type 'IProps'.
          Types of property 'type' are incompatible.
            Type 'string' is not assignable to type 'MouseEvent<string, MouseEvent> | KeyboardEvent<string>'.
  Overload 2 of 2, '(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void', gave the following error.
    Argument of type '({ type, target, key }: IProps) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'.
      Type '({ type, target, key }: IProps) => void' is not assignable to type 'EventListener'.
        Types of parameters '__0' and 'evt' are incompatible.
          Type 'Event' is not assignable to type 'IProps'.
            Types of property 'type' are incompatible.
              Type 'string' is not assignable to type 'MouseEvent<string, MouseEvent> | KeyboardEvent<string>'.

34     document.addEventListener("click", handleUserEvent);
                                          ~~~~~~~~~~~~~~~


use-close-menu.tsx:35:42 - error TS2769: No overload matches this call.
  Overload 1 of 2, '(type: "keydown", listener: (this: Document, ev: KeyboardEvent) => any, options?: boolean | AddEventListenerOptions): void', gave the following error.
    Argument of type '({ type, target, key }: IProps) => void' is not assignable to parameter of type '(this: Document, ev: KeyboardEvent) => any'.
      Types of parameters '__0' and 'ev' are incompatible.
        Type 'KeyboardEvent' is not assignable to type 'IProps'.
          Types of property 'type' are incompatible.
            Type 'string' is not assignable to type 'MouseEvent<string, MouseEvent> | KeyboardEvent<string>'.
  Overload 2 of 2, '(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void', gave the following error.
    Argument of type '({ type, target, key }: IProps) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'.
      Type '({ type, target, key }: IProps) => void' is not assignable to type 'EventListener'.

35     document.addEventListener("keydown", handleUserEvent);
                                            ~~~~~~~~~~~~~~~


use-close-menu.tsx:37:45 - error TS2769: No overload matches this call.
  Overload 1 of 2, '(type: "click", listener: (this: Document, ev: MouseEvent) => any, options?: boolean | EventListenerOptions): void', gave the following error.
    Argument of type '({ type, target, key }: IProps) => void' is not assignable to parameter of type '(this: Document, ev: MouseEvent) => any'.
      Types of parameters '__0' and 'ev' are incompatible.
        Type 'MouseEvent' is not assignable to type 'IProps'.
  Overload 2 of 2, '(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void', gave the following error.
    Argument of type '({ type, target, key }: IProps) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'.
      Type '({ type, target, key }: IProps) => void' is not assignable to type 'EventListener'.

37       document.removeEventListener("click", handleUserEvent);
                                               ~~~~~~~~~~~~~~~


use-close-menu.tsx:38:44 - error TS2769: No overload matches this call.
  Overload 1 of 2, '(type: "keydown", listener: (this: Document, ev: KeyboardEvent) => any, options?: boolean | AddEventListenerOptions): void', gave the following error.
    Argument of type '({ type, target, key }: IProps) => void' is not assignable to parameter of type '(this: Document, ev: KeyboardEvent) => any'.
  Overload 2 of 2, '(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void', gave the following error.
    Argument of type '({ type, target, key }: IProps) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'.
      Type '({ type, target, key }: IProps) => void' is not assignable to type 'EventListener'.

38       document.addEventListener("keydown", handleUserEvent);
                                              ~~~~~~~~~~~~~~~



Found 7 errors.

However, I'm going round in circles with TS error handling here. Can someone help me understand how to resolve these errors?

1
  • type is an event object, not a string. Commented Nov 11, 2022 at 17:04

1 Answer 1

3

Can someone help me understand how to resolve these errors?

tl;dr - key property does not exist on the MouseEvent.

Personally I'd remove IProps completely and just pass whole event object to the handleUserEvent function. Then, we could implement a type guard to check wheter the current event is mouse or keyboard.

import React from 'react';

type Evt = KeyboardEvent | MouseEvent;

function isKeyboardEvent(event: Evt): event is KeyboardEvent {
  return 'key' in event;
}

function useCloseMenu(
  ref: React.MutableRefObject<any>,
  isMenuOpen: boolean,
  callback: () => void
): void {
  React.useEffect(() => {
    function handleUserEvent(event: Evt): void {
        const { type, target } = event;

      switch (type) {
        case "click":
          if (!ref.current?.contains(target) && isMenuOpen) {
            callback();
          }

        case "keydown":
          if (isKeyboardEvent(event) && event.key === ("Escape" || "Esc") && isMenuOpen) {
            callback();
          }
      }
    }

    document.addEventListener("click", handleUserEvent);
    document.addEventListener("keydown", handleUserEvent);
    return () => {
      document.removeEventListener("click", handleUserEvent);
      document.addEventListener("keydown", handleUserEvent);
    };
  }, [isMenuOpen]);
}

Typescript playground

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

4 Comments

Thanks for the quick reply. This is helpful and interesting to see how you have helped to resolve the earlier errors but how would you also resolve the latter ones related to the addEventListener?
@Dan My bad - posted wrong code and link. Updated answer.
I also think it's worth mentioning that document.addEventListener simply doesn't accept a function with the custom IProps interface as a parameter. This link explains what the callback (handleUserEvent in this case) should look like.
Thanks this works and certainly helps my understanding.

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.