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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 04:57:10