skip to Main Content

I have this component in 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;

In this component, one of the props that is onChange, takes two different types of function, and depending on the type of function I want to run the function.
For example when the type is of ((id: number) => void) I want to run (onChange as (id: number) => void)(selectedValue) and when the type is of HTMLSelectElement i want to pass the event.
I can achieve this by passing one of the additional props but is there any way to differentiate these two functions?

I tried a couple of ways like checking type and instance but it didn’t work.
Any suggestions?

2

Answers


  1. This is tricky (or impossible) to do the way you are approaching this because at runtime both functions become id => void because when the code is transpiled all type information is lost. typeof and instanceof are operations that are evaluated at runtime.

    The dirty way to do it is to add an additional property to SelectProps

    type SelectProps<T> = {
      options: T[];
      value: number;
      onChange?: (id: number) => void;
      onChangeEvent?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
      labelKey: keyof T;
      labelValue: keyof T;
      defaultText: string;
    };
    
    const Select = <T extends object>({
      options,
      value,
      onChange,
      onChangeEvent,
      labelKey,
      labelValue,
      defaultText,
    }: SelectProps<T>) => {
      const handleOptionChange = (event: ChangeEvent<HTMLSelectElement>) => {
        const selectedValue = parseInt(event.target.value);
        if (onChange !== undefined) {
            onChange(selectedValue);
        } else if (onChangeEvent !== undefined) {
            onChangeEvent(event);
        }
      };
    

    You may make it into a type union e.g.

    type SelectProps<T> = {
      options: T[];
      value: number;
      labelKey: keyof T;
      labelValue: keyof T;
      defaultText: string;
    }&({ onChange: (id: number) => void; }|{ onChangeEvent: (e: React.ChangeEvent<HTMLSelectElement>) => void; });
    

    This may better clarify that you need one or the other (not none or both)

    Login or Signup to reply.
  2. You can write a custom type guard function like this:

    const isNumberFunction = (value: any): value is (id: number) => void => {
      return typeof value === "function";
    };
    
    
    const handleOptionChange = (event: ChangeEvent<HTMLSelectElement>) => {
      const selectedValue = parseInt(event.target.value);
      if (typeof onChange === "function") {
        if (isNumberFunction(onChange)) {
          onChange(selectedValue);
        } else {
          onChange(event);
        }
      }
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search