How to set type for event.target.value?

I have this Select component:

type SelectProps = {
  title: string;
  name: string;
  items: string[];
  onChange: (e: ChangeEvent<HTMLSelectElement>) => void;
  defValue: string;
};

const Select: FC<SelectProps> = ({ title, name, items, onChange, defValue }) => {
  return (
    <>
      <label htmlFor={name}>
        {title}:
      </label>
      <select name={name} onChange={onChange} defaultValue={defValue}>
        {items.map((item) => (
          <option value={item} key={item}>
            {item}
          </option>
        ))}
      </select>
    </>
  );
};

and I'm handling onChange with this function:

const onThemeChange = (e: ChangeEvent<HTMLSelectElement>) => {
  const theme = e.target.value;
  setTheme(theme)
};

...

<Select
  title='Theme'
  defValue={props.theme}
  name='theme'
  items={['light', 'dark']}
  onChange={onThemeChange}
/>

My setTheme action creator accepts argument with type 'light' | 'dark', so I'm getting an error:

Argument of type 'string' is not assignable to parameter of type '"light" | "dark"'

What is the best way to solve this issue?

Upvotes: 0

Views: 4456

Answers (2)

Karol Majewski
Karol Majewski

Reputation: 25850

There is away, but it requires a little trick.

First, let's recognize the relationships between types in your SelectProps:

  • the items are string literals
  • in onChange, the event will have a target.value equal to one of your items
  • the defValue should also be one of the items

To express these constraints, we need to use a generic interface.

type SelectProps<T extends string> = {
  title: string;
  name: string;
  items: T[];
  onChange: (e: ChangeEvent<HTMLSelectElement> & { target: { value: T }}) => void;
  defValue: DeferTypeInference<T>;
};

const Select = function<T extends string>({ title, name, items, onChange, defValue }: SelectProps<T>) {
  return (
    <>
      <label htmlFor={name}>
        {title}:
      </label>
      <select name={name} onChange={onChange} defaultValue={defValue}>
        {items.map((item) => (
          <option value={item} key={item}>
            {item}
          </option>
        ))}
      </select>
    </>
  );
};

We have achieved everything.

<Select
  title='Theme'
  defValue="light" // only "light" or "dark" are accepted
  name='theme'
  items={['light', 'dark']}
  onChange={event => event.target.value} // event.target.value is "light" or "dark"
/>

Note the use of a type called DeferTypeInference<T>. If you're curious why it's there, check out this answer.

Upvotes: 2

wentjun
wentjun

Reputation: 42596

The quickest way of doing so would be to do type assertions.

Assuming that this is how you initialised the state,

type ThemeState = 'light' | 'dark';

const [theme, useTheme] = useState<ThemeState>('light');

And then, on your onThemeChange method, you will assert the value as ThemeState

const theme = e.target.value as ThemeState;

Upvotes: 0

Related Questions