Reyhan
Reyhan

Reputation: 31

How can I type html select element so event.target.value refers to the option value type?

I have a component called Select and it has options, value and onChange props. options is an array of objects with label and value attributes. here is my Select.tsx code

import * as React from "react";
import type { ChangeEventHandler, ReactElement } from "react";

export interface OptionType {
  value: string | number | null;
  label: string;
}

export const Select = ({
  value,
  onChange,
  options
}: {
  value: string | number | null;
  onChange: ChangeEventHandler<HTMLSelectElement>;
  options: OptionType[];
}): ReactElement => (
  <select value={value ?? ""} onChange={onChange}>
    {options.map((option) => (
      <option value={option.value ?? ""} key={option.value}>
        {option.label}
      </option>
    ))}
  </select>
);

I used the Select component like this, the problem is at the onChange event handler below. I want event.target.value to have the type of UnitType.

import * as React from "react";
import { Select } from "./Select";
const { useState } = React;

type UnitType = "cm" | "m" | "km";
interface UnitOptionType {
  label: string;
  value: UnitType;
}

// Select options array with a custom type
const timeOptions: UnitOptionType[] = [
  { label: "Centimeter", value: "cm" },
  { label: "Meter", value: "m" },
  { label: "Kilometre", value: "km" }
];

export const App = () => {
  const [value, setValue] = useState<UnitType | null>(null);

  return (
    <div>
      <Select
        options={timeOptions}
        value={value}
        onChange={(event) => {
          // I WANT this to have the type of UnitType
          // but it is string
          setValue(event.target.value)
        }}
      />
    </div>
  );
};

You can view my code on this Codesandbox link:

https://codesandbox.io/s/xenodochial-matan-i3p9z?file=/src/App.tsx:0-765

Upvotes: 2

Views: 3870

Answers (1)

Alex Wayne
Alex Wayne

Reputation: 187312

The value of HTMLSelectElement is always going to be a string. It's typed that way in the lib.dom standard type library that ships with typescript. There isn't much you can do to change that.


In this case I would make the <Select> component generic so it knows the types of its options.

Then make the onChange callback receive this type, rather than the whole event.

Lastly, you cast the value to that type, since you the the value will be the value of one of the options.

Putting that together you get:

export interface OptionType<T extends string | number | null> {
  value: T;
  label: string;
}

export const Select = <T extends string | number | null>({
  value,
  onChange,
  options
}: {
  value: T;
  onChange: (newValue: T) => void;
  options: OptionType<T>[];
}): ReactElement => (
  <select value={value ?? ""} onChange={(event) => onChange(event.target.value as T)}>
    {options.map((option) => (
      <option value={option.value ?? ""} key={option.value}>
        {option.label}
      </option>
    ))}
  </select>
);

Now onChange provides T, which means the union of values of the options, so you can pass that value directly to setState, without having to proxy through the event object at all.

      <Select
        options={timeOptions}
        value={value}
        onChange={setValue}
      />

Typescript playground with the above code and no type errors.


One final note: You use the the type for the values as string | number | null, but as stated above, .value will always return a string, so you will never get null or number from that. So supporting those types will be more complicated and you can't do it in typeland alone since you need to know how to convert string to the type you want at runtime.

Upvotes: 4

Related Questions