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
相关产品推荐
相关产品推荐

