I'm trying to build a simple Select component, that can handle both singular and multiple selections. I want it to be typesafe, so as far as I understood I can set the type of component's value depending on its isMultiselect
property, so this is how it went:
The value type:
type ISelectItem = {label: string, value: string};
// conditional type - or at least how I thought it should work
type ISelectValue<TIsMultiselect extends boolean> = TIsMultiselect extends true ? Array<ISelectItem> : ISelectItem;
The component props type:
type IAutocompleteSelectProps<TIsMultiselect extends boolean> = {
items: ISelectItem[]; // items to select from
value?: ISelectValue<TIsMultiple>; // component value
setValue: (value: ISelectValue<TIsMultiple>) => void; // function to set the value from the outside
isMultiselect: TIsMultiselect; // flag defining if we have singular or multiple values
}
And the component itself (only relevant part):
function AutocompleteSelect<TIsMultiselect extends boolean>({
items,
onSearch,
setValue,
value,
isMultiselect,
}: IAutocompleteSelectProps<TIsMultiselect>) {
const selectValue = (item: ISelectItem) => {
if (isMultiselect) {
value
// TS2345: Argument of type 'any[]' is not assignable to parameter of type 'ISelectValue ',
// and Type 'ISelectValue ' must have a '[Symbol.iterator]()' method that returns an iterator.
? setValue([...value, item])
// TS2345: Argument of type 'ISelectItem []' is not assignable to parameter of type 'ISelectValue '.
: setValue([item]);
} else {
// TS2345: Argument of type 'ISelectItem ' is not assignable to parameter of type 'ISelectValue '.
setValue(item);
}
}
// ...rest of the component logic
}
I'd expect TS to understand, that if isMultiselect
is true, so the value should contain an array of items, and only one item otherwise. Instead, it tells me that I misunderstood something here, but after extensive googling I can't figure out what :(
Here's the playground
Instead of using generic, please consider to do the following:
type IAutocompleteSelectSingleProps = {
isMultiselect: false;
value?: ISelectItem;
setValue: (value: ISelectItem) => void;
};
type IAutocompleteSelectMultiProps = {
isMultiselect: true;
value?: ISelectItem[];
setValue: (value: ISelectItem[]) => void;
};
type IAutocompleteSelectProps = IAutocompleteSelectBaseProps &
(IAutocompleteSelectMultiProps | IAutocompleteSelectSingleProps);
This way, your props will always have IAutocompleteSelectBaseProps
, and depending on the value of isMultiselect
, you will have either IAutocompleteSelectMultiProps
or IAutocompleteSelectSingleProps
.
Also, depending on whether you use strictNullChecks
tsconfig option or not, you will have to add if(isMultiselect === true)
to make it work as expected. You will have to add ===
in case if you do not use strictNullChecks
.