I am trying to restrict values for a property (or react props in my case) based on what values are given in another property.
I have my Option interface
interface Option {
value: string;
label: string;
}
I then have my SelectInputProps interface
interface SelectInputProps<T extends Option> {
options: T[];
defaultValue: string;
}
What I want is to the make defaultValue only accept any string present in options values.
interface SelectInputProps<T extends Option> {
options: T[];
defaultValue: ThisShouldOnlyAcceptStringsInOptionsValues;
}
I have read this article which is close to my use case, here, but it gets types from already defined arrays. I do not know what values will be passed, hence the generic.
I did try
interface SelectInputProps<T extends Option> {
options: T[];
defaultValue: Array<T>[number]['value'];
}
but this only restricts it to any string, which is no different from the first snippet.
I may be going in the complete wrong direction here and there may be a simpler way, all help appreciated.
Edit
Usage in React component:
const SelectInput = <T extends Option>({
options,
defaultValue,
}: SelectInputProps<T>) => {
const [value, setValue] = React.useState(defaultValue);
const [isSelecting, setIsSelecting] = React.useState(false);
const optionElements = options.map((option) => (
<option value={option.value} key={option.value}>
{option.label}
</option>
));
return (
<div className="relative">
<div className="flex justify-between items-center">
<p>{options.find((op) => op.value === value)?.label}</p>
<MdExpandMore size={'1.5rem'} />
</div>
</div>
);
};
CodePudding user response:
Type inference is a tricky subject in TypeScript. The issue here is not only the generic type itself but the function declaration too. When you pass an array like [{value: "abc", label: "abc"}]
to a function, TypeScript has to decide how this array literal will be interpreted.
TypeScript will default widen types like this by default. So it infers the array literal
[{value: "abc", label: "abc"}]
as
{ value: string, label: string }[]
While this technically is a perfectly valid description of the passed array literal, we lose a lot of type information. It is not possible for us to get the type of value
since it just a string
now.
Luckily for us, there are certain tricks to force TypeScript to infer types as narrow as we need them to be.
Let's start with the interfaces.
interface Option<V extends string> {
value: V;
label: string;
}
interface SelectInputProps<T extends Option<V>[], V extends string> {
options: T;
defaultValue: T[number]["value"];
}
We will introduce a new generic type V
which will hold information about the string literal type for value
.
When we declare the function, we also have to add V
.
const SelectInput = <T extends Option<V>[], V extends string>({
options,
defaultValue,
}: SelectInputProps<T, V>) => {
/* ... */
};
Here some test cases:
function test(){
return (<div>
<SelectInput options={
[{value: "a", label: "a"}, {value: "b", label: "b"}]
} defaultValue="wrong"></SelectInput> // Error: Type '"wrong"' is not assignable to type '"a" | "b"'
<SelectInput options={
[{value: "a", label: "a"}, {value: "b", label: "b"}]
} defaultValue="a"></SelectInput>
</div>)
}
CodePudding user response:
You need to bring the "extends" keyword outside of the <> carrots (ex. interface SelectInputProps extends Option
)