Search code examples
reactjstypescript

Type check fails when passing variables to component but works when passing values ​directly


I recently developed a Select component using Typescript and React. It has two modes to choose from: single-select and multiple-select. Everything works fine in single-select mode, but in multiple-select mode, it will appear that TS cannot correctly infer the parameter type. Here is my code:

type SelectProps<T> =
  | {
      value: T | undefined | null;
      multiple?: undefined;
      onChange?: (value: T | null) => void;
    }
  | {
      value: T[];
      multiple: true;
      onChange?: (value: T[]) => void;
    };

function Select<T>(props: SelectProps<T>) {
  return <div>...</div>;
}

const n = [1];
// Error:
// Type '{ value: number[]; multiple: true; onChange: (v: number[][]) => void; }' is not assignable to type 'IntrinsicAttributes & SelectProps<number[]>'.
//   Types of property 'value' are incompatible.
//     Type 'number[]' is not assignable to type 'number[][]'.
//       Type 'number' is not assignable to type 'number[]'.
const a = <Select value={n} multiple onChange={(v) => {}}></Select>;

// This works fine
const b = <Select value={[1]} multiple onChange={(v) => {}}></Select>;

I asked ChatGPT, and it suggested that I explicitly specify the <number> type, which did work:

const a = <Select<number> value={n} multiple onChange={(v) => {}}></Select>;

but when I asked it how to refactor the code so that TS can automatically infer the correct type, the answer it gave still had the same problem.

Is there any way to make TS's automatic inference work without explicitly specify the <number>?


Solution

  • You can test whether T itself is an Array, instead of having an Array of T. Though note that this makes multiple become the dependent variable and value become the independent variable (i.e value decide the type, error will be shown on multiple).

    type SelectProps<T> = T extends Array<any> ? | {
      value: T;
      multiple: true;
      onChange?: (value: T) => void;
    } : {
      value: T | undefined | null;
      multiple?: undefined | false;
      onChange?: (value: T | null) => void;
    };
    

    If you want multiple to take the priority instead, you can write

    type SelectProps<T,U> = U extends true ? | {
      value: T[];
      multiple: U;
      onChange?: (value: T[]) => void;
    } : {
      value: T | undefined | null;
      multiple?: U;
      onChange?: (value: T | null) => void;
    };
    

    then multiple will decide the type, error will be shown on value