如何在TypeScript React组件中区分两种不同类型的onChange函数
问题描述
我有一个如下的React组件:
import { ChangeEvent } from "react"; type SelectProps<T> = { options: T[]; value: number; onChange: | ((id: number) => void) | ((e: React.ChangeEvent<HTMLSelectElement>) => void); labelKey: keyof T; labelValue: keyof T; defaultText: string; }; /** * Select component to show and to select the options. * The value that gets selected is the id. * The labelKey and labelValue is the keyof T, that is the key of the options. * * @param options List of objects to display in the Select. * @param value The selected value. * @param onChange The onChange method to change the value when selected. * @param labelKey The text that we want to select when we choose an option. * @param labelValue The text that we want to show in the dropdown. * @param defaultText The text to display when the value is 0. * * @returns Select component. */ const Select = <T extends object>({ options, value, onChange, labelKey, labelValue, defaultText, }: SelectProps<T>) => { const handleOptionChange = (event: ChangeEvent<HTMLSelectElement>) => { const selectedValue = parseInt(event.target.value); if (typeof onChange === "function") { if (onChange instanceof Function) { (onChange as (id: number) => void)(selectedValue); } else { (onChange as (e: React.ChangeEvent<HTMLSelectElement>) => void)(event); } } }; return ( <div> <select value={value} onChange={handleOptionChange} className="p-2 border w-full cursor-pointer bg-white text-gray-dark border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 overflow-y-auto max-h-32" > <option value={0}>Select a {defaultText}</option> {options.map((option) => ( <option key={option[labelKey]} value={option[labelKey]} className="whitespace-nowrap" > {option[labelValue]} </option> ))} </select> </div> ); }; export default Select;
这个组件的onChange属性支持两种不同类型的函数,我希望根据函数类型分别调用:当函数是((id: number) => void)类型时,传入选中值id;当函数是接收React.ChangeEvent<HTMLSelectElement>的类型时,传入事件对象。我可以通过新增props实现,但想知道是否有方法直接区分这两种函数?尝试过类型和实例判断但未成功,求可行方案。
可行方案
由于TypeScript类型信息在编译后会被擦除,运行时无法直接通过类型系统区分这两个函数,以下是几种实用的实现思路:
1. 给函数添加自定义标识属性
在定义传入的onChange函数时,手动添加一个自定义属性标记函数类型,组件内部通过该属性区分调用方式:
使用组件时的示例:
// 接收number的函数 const handleIdChange = (id: number) => { /* 业务逻辑 */ }; handleIdChange.__handlerType = "id"; // 接收ChangeEvent的函数 const handleEventChange = (e: React.ChangeEvent<HTMLSelectElement>) => { /* 业务逻辑 */ }; handleEventChange.__handlerType = "event"; // 传入组件 <Select onChange={handleIdChange} {...otherProps} />
组件内的处理逻辑:
const handleOptionChange = (event: ChangeEvent<HTMLSelectElement>) => { const selectedValue = parseInt(event.target.value); if (typeof onChange === "function") { // @ts-ignore 忽略TypeScript对自定义属性的类型检查 if (onChange.__handlerType === "event") { (onChange as (e: React.ChangeEvent<HTMLSelectElement>) => void)(event); } else { (onChange as (id: number) => void)(selectedValue); } } };
这种方式是最可靠的,因为直接给函数打了明确的运行时标记。
2. 通过函数字符串特征匹配
将函数转为字符串后,通过匹配参数的类型特征区分。比如接收ChangeEvent的函数,其字符串内容通常会包含React.ChangeEvent或ChangeEvent关键字:
const handleOptionChange = (event: ChangeEvent<HTMLSelectElement>) => { const selectedValue = parseInt(event.target.value); if (typeof onChange === "function") { const funcStr = onChange.toString(); if (funcStr.includes("React.ChangeEvent") || funcStr.includes("ChangeEvent")) { (onChange as (e: React.ChangeEvent<HTMLSelectElement>) => void)(event); } else { (onChange as (id: number) => void)(selectedValue); } } };
这种方式无需使用者额外处理函数,但如果函数经过压缩或参数类型未显式标注,可能会失效,属于折中方案。
3. 拆分props类型,利用TypeScript重载优化编译时检查
虽然无法在运行时直接区分,但可以通过拆分props类型,让组件使用者在编译时只能传入其中一种函数类型,同时保留运行时的兼容处理:
// 拆分两种props类型 type SelectWithIdHandlerProps<T> = { options: T[]; value: number; onChange: (id: number) => void; labelKey: keyof T; labelValue: keyof T; defaultText: string; }; type SelectWithEventHandlerProps<T> = { options: T[]; value: number; onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void; labelKey: keyof T; labelValue: keyof T; defaultText: string; }; // 重载组件签名 function Select<T extends object>(props: SelectWithIdHandlerProps<T>): JSX.Element; function Select<T extends object>(props: SelectWithEventHandlerProps<T>): JSX.Element; function Select<T extends object>({ options, value, onChange, labelKey, labelValue, defaultText, }: SelectWithIdHandlerProps<T> | SelectWithEventHandlerProps<T>) { const handleOptionChange = (event: ChangeEvent<HTMLSelectElement>) => { const selectedValue = parseInt(event.target.value); const funcStr = onChange.toString(); if (funcStr.includes("ChangeEvent")) { (onChange as (e: React.ChangeEvent<HTMLSelectElement>) => void)(event); } else { (onChange as (id: number) => void)(selectedValue); } }; // 组件返回部分不变 return ( <div> <select value={value} onChange={handleOptionChange} className="p-2 border w-full cursor-pointer bg-white text-gray-dark border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 overflow-y-auto max-h-32" > <option value={0}>Select a {defaultText}</option> {options.map((option) => ( <option key={option[labelKey]} value={option[labelKey]} className="whitespace-nowrap" > {option[labelValue]} </option> ))} </select> </div> ); }
这种方式可以在编译时避免使用者传入不符合预期的函数类型,同时保留运行时的兼容处理。
内容的提问来源于stack exchange,提问作者prajun7
相关产品推荐
相关产品推荐

