Search code examples
typescriptconditional-types

Typescript conditional types aren't inferred as expected


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


Solution

  • 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.

    playground