You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React TypeScript解构事件对象类型声明错误排查求助

问题

我想要创建一个可复用的自定义Hook,实现用户点击节点外部或按下ESC键时关闭菜单。从未使用过TypeScript,希望解构事件对象而非直接使用(例如event.type),并为对象键创建类型声明。现有代码如下:

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;

运行代码后出现以下TypeScript错误:

use-close-menu.tsx:21:14 - 错误 TS2678: 类型'string'不能与类型'MouseEvent<string, MouseEvent> | KeyboardEvent<string>'比较。

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

use-close-menu.tsx:26:14 - 错误 TS2678: 类型'string'不能与类型'MouseEvent<string, MouseEvent> | KeyboardEvent<string>'比较。

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

use-close-menu.tsx:28:15 - 错误 TS2367: 此条件始终返回'false',因为类型'KeyboardEvent<string>'与'string'无重叠。

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

use-close-menu.tsx:34:40 - 错误 TS2769: 无匹配的重载调用。
  重载1(共2个):'(type: "click", listener: (this: Document, ev: MouseEvent) => any, options?: boolean | AddEventListenerOptions): void',给出以下错误。
    参数类型'({ type, target, key }: IProps) => void'不能赋值给参数类型'(this: Document, ev: MouseEvent) => any'。
      参数'__0'和'ev'的类型不兼容。
        类型'MouseEvent'不能赋值给类型'IProps'。
          属性'type'的类型不兼容。
            类型'string'不能赋值给类型'MouseEvent<string, MouseEvent> | KeyboardEvent<string>'。
  重载2(共2个):'(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void',给出以下错误。
    参数类型'({ type, target, key }: IProps) => void'不能赋值给参数类型'EventListenerOrEventListenerObject'。
      类型'({ type, target, key }: IProps) => void'不能赋值给类型'EventListener'。
        参数'__0'和'evt'的类型不兼容。
          类型'Event'不能赋值给类型'IProps'。
            属性'type'的类型不兼容。
              类型'string'不能赋值给类型'MouseEvent<string, MouseEvent> | KeyboardEvent<string>'。

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


use-close-menu.tsx:35:42 - 错误 TS2769: 无匹配的重载调用。
  重载1(共2个):'(type: "keydown", listener: (this: Document, ev: KeyboardEvent) => any, options?: boolean | AddEventListenerOptions): void',给出以下错误。
    参数类型'({ type, target, key }: IProps) => void'不能赋值给参数类型'(this: Document, ev: KeyboardEvent) => any'。
      参数'__0'和'ev'的类型不兼容。
        类型'KeyboardEvent'不能赋值给类型'IProps'。
          属性'type'的类型不兼容。
            类型'string'不能赋值给类型'MouseEvent<string, MouseEvent> | KeyboardEvent<string>'。
  重载2(共2个):'(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void',给出以下错误。
    参数类型'({ type, target, key }: IProps) => void'不能赋值给参数类型'EventListenerOrEventListenerObject'。
      类型'({ type, target, key }: IProps) => void'不能赋值给类型'EventListener'。

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


use-close-menu.tsx:37:45 - 错误 TS2769: 无匹配的重载调用。
  重载1(共2个):'(type: "click", listener: (this: Document, ev: MouseEvent) => any, options?: boolean | EventListenerOptions): void',给出以下错误。
    参数类型'({ type, target, key }: IProps) => void'不能赋值给参数类型'(this: Document, ev: MouseEvent) => any'。
      参数'__0'和'ev'的类型不兼容。
        类型'MouseEvent'不能赋值给类型'IProps'。
  重载2(共2个):'(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void',给出以下错误。
    参数类型'({ type, target, key }: IProps) => void'不能赋值给参数类型'EventListenerOrEventListenerObject'。
      类型'({ type, target, key }: IProps) => void'不能赋值给类型'EventListener'。

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


use-close-menu.tsx:38:44 - 错误 TS2769: 无匹配的重载调用。
  重载1(共2个):'(type: "keydown", listener: (this: Document, ev: KeyboardEvent) => any, options?: boolean | AddEventListenerOptions): void',给出以下错误。
    参数类型'({ type, target, key }: IProps) => void'不能赋值给参数类型'(this: Document, ev: KeyboardEvent) => any'。
  重载2(共2个):'(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void',给出以下错误。
    参数类型'({ type, target, key }: IProps) => void'不能赋值给参数类型'EventListenerOrEventListenerObject'。
      类型'({ type, target, key }: IProps) => void'不能赋值给类型'EventListener'。

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


发现7个错误。

错误分析与修复方案

1. 核心类型声明错误

原IProps完全搞反了事件属性的类型:

  • type是事件类型字符串("click"/"keydown"),不是事件对象本身
  • target是鼠标事件的目标元素,键盘事件的target类型不同且无需单独处理
  • key是键盘事件的按键字符串,不是键盘事件对象

正确做法是定义联合类型区分两种事件的解构属性:

type UserEventProps = 
  | { type: "click"; target: EventTarget }
  | { type: "keydown"; key: string };

2. 事件监听类型不匹配

原handleUserEvent的参数类型和浏览器原生事件不兼容,需要让函数接收原生事件,再通过类型守卫解构对应属性。

3. 按键判断逻辑错误

key === ("Escape" || "Esc")是无效逻辑,"Escape" || "Esc"永远返回"Escape",应改为判断key属于两者之一。

4. 清理函数错误

原清理函数误写为addEventListener,应改为removeEventListener。

5. Hook依赖项缺失

原useEffect依赖数组仅包含isMenuOpen,ref和callback也需加入,避免闭包导致的逻辑错误。


修复后的完整代码

import * as React from "react";

// 定义联合类型,区分两种事件的解构属性
type UserEventProps = 
  | { type: "click"; target: EventTarget }
  | { type: "keydown"; key: string };

/**
 * 点击节点外部或按下ESC键时关闭菜单的自定义Hook
 */
function useCloseMenu(
  ref: React.MutableRefObject<HTMLElement | null>, // 替换any为精准的DOM元素类型
  isMenuOpen: boolean,
  callback: () => void
): void {
  React.useEffect(() => {
    function handleUserEvent(event: MouseEvent | KeyboardEvent): void {
      const { type } = event;
      
      // 类型守卫:处理点击事件
      if (type === "click") {
        const { target } = event;
        if (isMenuOpen && ref.current && !ref.current.contains(target as Node)) {
          callback();
        }
      } 
      // 类型守卫:处理键盘事件
      else if (type === "keydown") {
        const { key } = event;
        if (isMenuOpen && (key === "Escape" || key === "Esc")) {
          callback();
        }
      }
    }

    // 仅在菜单打开时添加监听,优化性能
    if (isMenuOpen) {
      document.addEventListener("click", handleUserEvent);
      document.addEventListener("keydown", handleUserEvent);
    }

    // 清理函数:移除所有监听
    return () => {
      document.removeEventListener("click", handleUserEvent);
      document.removeEventListener("keydown", handleUserEvent);
    };
  }, [ref, isMenuOpen, callback]); // 添加所有依赖项
}

export default useCloseMenu;

额外优化说明

  • 将ref类型从any改为React.MutableRefObject<HTMLElement | null>,符合实际DOM绑定场景
  • 增加isMenuOpen判断,避免菜单关闭时添加不必要的事件监听
  • 使用类型守卫替代switch,让TypeScript自动窄化事件类型,确保属性访问安全
  • 修复清理函数的错误,保证组件卸载或依赖更新时正确移除监听

内容的提问来源于stack exchange,提问作者Dan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 09:10:32