Reputation: 31
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
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