skip to Main Content

I’m using a select list in React with TypeScript.

const allArtists = ["elvis", "dr dre"] as const;
type Artist = (typeof allArtists)[number];

function App() {
  const [artist, setArtist] = useState<Artist | "">("");

  function handleArtistChange(e: React.ChangeEvent<HTMLSelectElement>) {
    const newArtist = e.target.value as Artist | "";
    setArtist(newArtist);
  }
  
  return (
    <div>
      <select value={artist} onChange={handleArtistChange}>
        <option value={""}>Please choose</option>
        {allArtists.map((a) => (
          <option key={a} value={a}>
            {a}
          </option>
        ))}
      </select>
    </div>
  );
}

The code above isn’t great as if an illegal value is added as an option I don’t get TypeScript errors:

<option value={"ILLEGAL VALUE"}>Please choose</option>

I know one option would be to check if the string passed to handleArtistChange appears in allArtists. This also isn’t ideal as there is a runtime performance, and "ILLEGAL VALUE" still doesn’t produce a compile time error.

Is there a compile time solution that doesn’t use type casting?

2

Answers


  1. With some contortions and a couple helper components, at least:

    import * as React from "react";
    
    const allArtists = ["elvis", "dr dre"] as const;
    type Artist = typeof allArtists[number];
    
    interface TypedSelectChangeEvent<ValueT extends string> extends React.ChangeEvent<HTMLSelectElement> {
      target: EventTarget & (Omit<HTMLSelectElement, "value"> & { value: ValueT | "" });
    }
    
    function TypedOption<T extends string>(
      props: Omit<React.OptionHTMLAttributes<HTMLOptionElement>, "value"> & { value?: T | "" },
    ) {
      return <option {...props} />;
    }
    function TypedSelect<T extends string>(
      props: Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "value" | "onChange"> & {
        value?: T | "";
        onChange?: React.EventHandler<TypedSelectChangeEvent<T>>;
      },
    ) {
      return <select {...props} />;
    }
    
    function App() {
      const [artist, setArtist] = React.useState<Artist | "">("");
    
      function handleArtistChange(e: TypedSelectChangeEvent<Artist>) {
        setArtist(e.target.value);
      }
    
      return (
        <div>
          <TypedSelect value={artist} onChange={handleArtistChange}>
            <TypedOption<Artist> value="">Please choose</TypedOption>
            {allArtists.map((a) => (
              <TypedOption<Artist> key={a} value={a}>
                {a}
              </TypedOption>
            ))}
          </TypedSelect>
        </div>
      );
    }
    
    Login or Signup to reply.
  2. To do this, you need to create a custom option component to do this.

    const allArtists = ["elvis", "dr dre"] as const;
    type Artist = (typeof allArtists)[number];
    
    interface OptionType extends HTMLProps<HTMLOptionElement> {
      value: Artist;
    }
    
    function ArtistOption(props: OptionType): ReactNode {
      return <option {...props} />;
    }
    

    You need to use the ArtistOption component in place of option tag in your code.

    You can even generalize this with generic types. However, I find it more useful to keep it simple unless you are creating something reusable for multiple use cases.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search