Definitelytyped: [@types/react-select] Anything can be passed to Select

Created on 24 Apr 2019  路  3Comments  路  Source: DefinitelyTyped/DefinitelyTyped

If you know how to fix the issue, make a pull request instead.

  • [x] I tried using the @types/react-select package and had problems.
  • [x] I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript
  • [x] I have a question that is inappropriate for StackOverflow. (Please ask any appropriate questions there).
  • [x] [Mention](https://github.com/blog/821-mention-somebody-they-re-notified) the authors (see Definitions by: in index.d.ts) so they can respond.

    • Authors: @claasahl @jonfreedman

Hi,

Because of line 54 at Select.d.ts anything can be passed to select and typescript will not show any errors.

export type SelectComponentsProps = { [key in string]: any };

What is the purpose of that { [key in string]: any }; in Select types?

Most helpful comment

This one caused problems for me too, because I've made some wrapper components around React-Select, that override some props, and pass other props through. So I have a props definition that did that by using some helper types based on Pick and Extract. It looks something like this:

import { Props as ReactSelectProps } from 'react-select/lib/Select';

// Omit: Create a type that has all the keys of T except those named in K
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Merge: Create a type that combines types M and N, but where N has a field
// with the same name as M, use only N's definition of that field.
type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N;

// My wrapper component's props can't just `extend` ReactSelectProps, because
// the types for `value` and `onChange` are incompatible.
type WrappedSelectProps = Merge<ReactSeletProps, {
  name: string;
  value?: number;
  options: Array<{value: number, label: React.ReactNode}>,
  onChange: (selection?: number) => void;
}>;

This worked great before, but now it fails to provide any validation or Intellisense of the props to pass through to React-Select. That's because my utility types use keyof as their base (as many utility types do). And now that the React-Select props extend { [key in string]: any}, doing keyof on it just returns string.

Fortunately I found a workaround for it. You can extract the "known keys" from a mapped type without using keyof via a somewhat confusing mix of mapped types, conditional types, and type inference: https://stackoverflow.com/questions/51954558/how-can-i-remove-a-wider-type-from-a-union-type-without-removing-its-subtypes-in/51955852#51955852

type KnownKeys<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K;
} extends { [_ in keyof T]: infer U }
  ? U
  : never;

... still, it would be more convenient to just be able to use normal utility types like Pick and Extract. For my purposes (writing types that are based on the React-Select props) it would be enough if the defined props of the Select component were just defined as a separate exported type (sort of like SelectComponentsProps is):

export type SelectComponentsProps = { [key in string]: any };

export interface NamedProps<OptionType> {
    /* Aria label (for assistive tech) */
  'aria-label'?: string;
  /* Focus the control when it is mounted */
  autoFocus?: boolean;
  // ... etc
}

export type Props<OptionType = { label: string; value: string }> = NamedProps<
  OptionType
> &
  SelectComponentsProps;

All 3 comments

The thing is that if interface extends { [key in string]: any }; you can pass literally everything to that component. It somehow solves an issue with having custom props in select components but it has a lot of disadvantages, e.g. you can pass everything (even if you make typo typescript will not inform you) and intelisense is not working.

So, I've prepared a solution with a generic argument that can be used for custom props of select components (and is by default set to { [key in string]: any };).

If you don't pass second generic argument you can have any props in custom component just like now:

Zrzut ekranu 2019-04-26 o 16 17 39

If you pass an interface as second argument you can have type on that custom prop:

Zrzut ekranu 2019-04-26 o 16 18 27

This one caused problems for me too, because I've made some wrapper components around React-Select, that override some props, and pass other props through. So I have a props definition that did that by using some helper types based on Pick and Extract. It looks something like this:

import { Props as ReactSelectProps } from 'react-select/lib/Select';

// Omit: Create a type that has all the keys of T except those named in K
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Merge: Create a type that combines types M and N, but where N has a field
// with the same name as M, use only N's definition of that field.
type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N;

// My wrapper component's props can't just `extend` ReactSelectProps, because
// the types for `value` and `onChange` are incompatible.
type WrappedSelectProps = Merge<ReactSeletProps, {
  name: string;
  value?: number;
  options: Array<{value: number, label: React.ReactNode}>,
  onChange: (selection?: number) => void;
}>;

This worked great before, but now it fails to provide any validation or Intellisense of the props to pass through to React-Select. That's because my utility types use keyof as their base (as many utility types do). And now that the React-Select props extend { [key in string]: any}, doing keyof on it just returns string.

Fortunately I found a workaround for it. You can extract the "known keys" from a mapped type without using keyof via a somewhat confusing mix of mapped types, conditional types, and type inference: https://stackoverflow.com/questions/51954558/how-can-i-remove-a-wider-type-from-a-union-type-without-removing-its-subtypes-in/51955852#51955852

type KnownKeys<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K;
} extends { [_ in keyof T]: infer U }
  ? U
  : never;

... still, it would be more convenient to just be able to use normal utility types like Pick and Extract. For my purposes (writing types that are based on the React-Select props) it would be enough if the defined props of the Select component were just defined as a separate exported type (sort of like SelectComponentsProps is):

export type SelectComponentsProps = { [key in string]: any };

export interface NamedProps<OptionType> {
    /* Aria label (for assistive tech) */
  'aria-label'?: string;
  /* Focus the control when it is mounted */
  autoFocus?: boolean;
  // ... etc
}

export type Props<OptionType = { label: string; value: string }> = NamedProps<
  OptionType
> &
  SelectComponentsProps;

Similar problem to @agwells 鈥撀爀xtending { [key in string]: any } seems to break helpers like Omit/Pick/Extract that are necessary for creating wrapper components. Here's a minimal reproduction of the issue in TS playground.

Agree with the above recommendations to export the props separately (assuming extending { [key in string]: any } cannot be removed). In the meantime I've successfully used @agwells workaround like so:

import ReactSelect from 'react-select';

type KnownKeys<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends {[_ in keyof T]: infer U}
  ? U
  : never;

export type CustomSelectProps<T> = Pick<
  ReactSelect<T>['props'],
  KnownKeys<ReactSelect<T>['props']>
>;
Was this page helpful?
0 / 5 - 0 ratings

Related issues

jbreckmckye picture jbreckmckye  路  3Comments

JudeAlquiza picture JudeAlquiza  路  3Comments

Loghorn picture Loghorn  路  3Comments

victor-guoyu picture victor-guoyu  路  3Comments

jamespero picture jamespero  路  3Comments