react-select typescript not working properly when I substitute Select with a Component that returns Select

Viewed 65

So instead of using Select directly I thought it would be better to create a component named CustomSelect which will return Select with some properties prefilled.

The issue is that now when I use the CustomSelect component with property isMulti={false} typescript does not automatically understands that OnChange property should be of type

(newValue: SingleValue<IOption>, actionMeta: ActionMeta<IOption>) => void

instead of

(newValue: SingleValue<IOption> | MultiValue<IOption>, actionMeta: ActionMeta<IOption>) => void

Why is this happening and is there any good workaround?

CustomSelect.tsx

import Select, {
  components,
  OptionProps,
  ValueContainerProps,
  Props as SelectProps,
} from "react-select";
import { IOption } from "../../Table/Types";
import "./CustomSelect.scss";

//type IOption = {
//  value: any;
//  label: string;
//  icon?: string;
//};

const { Option: OptionCointainer, ValueContainer } = components;

function OptionWithIcon(props: OptionProps<IOption>) {
  const {
    label,
    data: { icon },
  } = props;
  return (
    <OptionCointainer {...props} className="selectOption">
      {!!icon && <img src={icon} alt={label} />}
      {label}
    </OptionCointainer>
  );
}

// react-select is buggy when you try to override rendered value component
// children is being used as a workaround
function ValueWithIcon(props: ValueContainerProps<IOption>) {
  const { getValue, children } = props;
  const { label, icon } = getValue()[0];
  return (
    <ValueContainer {...props} className="selectValue">
      <>
        {!!icon && <img src={icon} alt={label} />}
        {label}
        <span className="hiddenAbsolute">{children}</span>
      </>
    </ValueContainer>
  );
}

function CustomSelect(props: SelectProps<IOption>) {
  const { options } = props;
  return (
    <Select
      isDisabled={options ? options.length < 2 : true}
      components={{
        Option: OptionWithIcon,
        ValueContainer: ValueWithIcon,
      }}
      {...props}
    />
  );
}

export default CustomSelect;

Filters.tsx

      <Select
        id="currencyFrom"
        isMulti={false}
        value={state.fromCurrency}
        // if I hover on change type is (newValue: SingleValue<IOption>, actionMeta: ActionMeta<IOption>) => void
        onChange={onValueChange("fromCurrency")}
        options={someOptions}
        isSearchable={false}
      />
      <CustomSelect
        id="currencyTo"
        isMulti={false}
        value={state.toCurrency}
        // if I hover on change type is (newValue: SingleValue<IOption> | MultiValue<IOption>, actionMeta: ActionMeta<IOption>) => void
        onChange={onValueChange("toCurrency")}
        options={someOptions}
        isSearchable={false}
      />

Edit React Typescript (forked)

1 Answers

The SelectProps type you are trying to adapt is a generic type with 3 type arguments (defined here). Because you provide a type for the first argument, inference is disabled the other 2 arguments and Typescript sets them to their default values instead.

If you need inference to work, you cannot provide partial type arguments, I can see 2 solutions:

  • Get rid of the Option argument in SelectProps<Option> and let the Option type be inferred as well
function CustomSelect(props: SelectProps) {
  • If you need to enforce a specific Option type, create another generic type for your props that only takes the 2 last arguments and let typescript infer these:
type CustomSelectProps<
    IsMulti extends boolean = boolean,
    Group extends GroupBase<Option> = GroupBase<Option>
> = SelectProps<Option, IsMulti, Group>;

…

function CustomSelect(props: CustomSelectProps) {

Also, because you want this type of inference to happen when using the component, you should also make your component generic, applying the same logic to the StateManagedSelect component type (defined here).

This gives us the following component type definition, using the CustomSelectProps:

function CustomSelect<
    IsMulti extends boolean = false,
    Group extends GroupBase<IOption> = GroupBase<IOption>
>(props: SelectProps<IOption, IsMulti, Group>) {

Updated codesandbox: you'll see I had to apply the same logic to your other components as well, as all the types depend on each other.

Related