react-select: [V2] [Typescript] ValueType

The problem

I’m using React-Select v2 in typescript and the onChange event handler has a first parameter with a the type ValueType<T> which is declared like this in the typings:

export type ValueType<OptionType> = OptionType | OptionsType<OptionType> | null | undefined;

The problem is that, because of that I can’t declare my event handler as:

private handleChange = (selected: MyOption) => {
  /** Code **/
}

Instead I have to declare it like this:

private handleChange = (selected?: MyOption | MyOption[] | null) => {
  /** Code **/
}

I can also declare ValueType in my project, but that’s a bit too much.

Possible solution:

Maybe there are better ways but one of the solutions, which would still be a bit awkward, would be to export the ValueType type so we can at least use that and not declare the value type in that manner.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 27
  • Comments: 23 (4 by maintainers)

Most upvoted comments

I had the same issue when using a select that allows single-select only. I was getting the same error as @bradchristensen.

I solved this by defining a type and doing a type assertion before using the selected option.

import { ValueType } from "react-select/lib/types";

type OptionType = { label: string; value: number };

<Select
  onChange={(selectedOption: ValueType<OptionType>) => {
    const value = (selectedOption as OptionType).value;
    ...
  }}
  ...
/>

Hit this issue too and found this thread in my search for answers. The suggestions above didn’t resolve the issue for me, but I think helped point me in the right direction!

I was super confused about this for a while and then I realised it’s pretty easy to solve with a type check before you attempt to use the selectedOption. If you explicitly check whether selectedOption is an array, TypeScript then understands that it is not possible for the value to be an array from that point forward and allows you to use it like you expect to.

<Select
  onChange={selectedOption => {
    if (Array.isArray(selectedOption)) {
      throw new Error("Unexpected type passed to ReactSelect onChange handler");
    }

    doSomethingWithSelectedValue(selectedOption.value);
  }}
  ...
/>

For reference, below is the error I was getting, which goes away when the Array.isArray check is uncommented:

The following worked for me using a multiselect:

<Select
	placeholder={"Select Device..."}
	isMulti
	options={generateDeviceOptions()}
	getOptionValue={getOptionValue}
	onChange={(selectedOption: ValueType<SelectedDevice>, e: ActionMeta) => handleDeviceChange((selectedOption as SelectedDevice[]), e)}
	value={props.devices}
/>
	
function handleDeviceChange (selectedOption: SelectedDevice[], e: ActionMeta) {
	console.log(selectedOption);
	props.setDevices(selectedOption)
	console.log(e);
}

type SelectedDevice = {
   label: any;
   value: any;
}
	

Hopefully this will help others who stumble upon this issue.

using: “react-select”: “^3.0.4” “@types/react-select”: “^3.0.0”

The isArray check no longer works for me

Property 'value' does not exist on type '{ value: string; label: string; } | OptionsType<{ value: string; label: string; }>'.'

I think the type definitions have gotten a little more complex in v3 and now typescript no longer can infer that isArray === false should reject the OptionsType type.

Also, note to everyone else in this thread: Avoid using as type-casting in TypeScript. It bypasses the safety of the type system and it’s a good way to introduce runtime errors. The as keyword does not perform any runtime checks and code further along could break and it becomes a nightmare tracking down the bug.

Not trying to be an ass; I generally agree with you. I would like to point out, however, that this whole thing can be obviated by being more specific about prop types and template types. The type that gets called with onChange is limited by the prop types, so there could be a template parameter specifying whether this is a multi-select, an optional select, etc. and then the type of value type could be specific instead of just “It could be an array, a value, null, or undefined”. In that case, you’d be able to assume that the parameter is what you expect it to be. So using as here is kind of just bypassing the bypass.

Also, yeah, not that I’m personally willing to take the time to do it, but I do think the ideal solution involves doing compile-time matching of prop types to deduce what the ValueType<OptionType> actually is in contexts like onChange where it should only ever be called with one value in a non-optional, non-multi select.

Now that Array.isArray doesn’t seem to be working, I’m just using 'length' in option:

    (option: ValueType<{ label: string; value: string }>) => {
      if (!option) { // shouldn't happen ever I don't think
        return;
      } else if ('length' in option) { // shouldn't happen ever I don't think
        if (option.length > 0) {
          // same as below
        }
        return;
      } else {
        // only meaningful branch
      }

@tony

Here is the default type for OptionType:

export interface Props<OptionType = { label: string; value: string }> extends SelectComponentsProps {

From: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-select/src/Select.d.ts#L55

And here is the type for ValueType<OptionType>:

export type OptionsType<OptionType> = ReadonlyArray<OptionType>;
export type ValueType<OptionType> = OptionType | OptionsType<OptionType> | null | undefined;

From: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-select/src/types.d.ts#L13

Unfortunately, the react-select type definitions provided by DefinitelyTyped do not export the default OptionType because I believe it can be overridden and defined by the user somehow. But in our pdx-connect project, we just used the default OptionType and copied the type into our own definitions:

export interface OptionType {
    label: string;
    value: string;
}

Now, the onChange callback is defined as producing a ValueType<OptionType> as the first parameter:

onChange?: (value: ValueType<OptionType>, action: ActionMeta) => void;

From: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-select/src/Select.d.ts#L168

But the ValueType<OptionType> is a complex union type and I figured it would be easier to deal with if it was just a OptionType[]. So, I create a simple OptionType.resolve utility function to perform this conversion wherever it was needed and to avoid duplicating code:

export namespace OptionType {

    /**
     * Resolves a ValueType into an array of OptionType.
     * @param value The ValueType to resolve.
     */
    export function resolve(value: ValueType<OptionType>): OptionType[] {
        let optionTypes: OptionType[];
        if (value == null) {
            optionTypes = [];
        } else if (Array.isArray(value)) {
            optionTypes = value;
        } else {
            optionTypes = [value];
        }
        return optionTypes;
    }
    
}

This resolve function simply performs “type differentiating” which you can read about here: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types

This is essentially what has been suggested earlier in this thread: Use Array.isArray to check the type and handle it accordingly. I just packaged it up in a nice utility function that also checks for null or undefined and returns a single concrete type.

Ideally, the author of the react-select should add this utility function to his library so everyone can use it. I’m okay to release this snippet under MIT so others can use it and hopefully it gets put directly into the react-select library.

Also, note to everyone else in this thread: Avoid using as type-casting in TypeScript. It bypasses the safety of the type system and it’s a good way to introduce runtime errors. The as keyword does not perform any runtime checks and code further along could break and it becomes a nightmare tracking down the bug.

I had the same issue when using a select that allows single-select only. I was getting the same error as @bradchristensen.

I solved this by defining a type and doing a type assertion before using the selected option.

import { ValueType } from "react-select/lib/types";

type OptionType = { label: string; value: number };

<Select
  onChange={(selectedOption: ValueType<OptionType>) => {
    const value = (selectedOption as OptionType).value;
    ...
  }}
  ...
/>

The solution from @plotka worked for me but it leaves the feeling that something is not right…

Is there a convenient solution without Array.isArray or as OptionType checking? Is it going to be implemented?

Option 1, intuitively expected, value as is

Doesn’t work because React-Select doesn’t know what value we are passing

Screenshot from 2019-07-23 12-39-40

Option 2 (by @ericmackrodt and @bradchristensen), with explicit types in handler signature

Doesn’t work because IContact[] is not the same as ReadonlyArray<IContact>

Screenshot from 2019-07-23 12-39-14

Screenshot from 2019-07-23 12-38-37

Option 3 (by @plotka), using as coercion

Though it works without errors, using as is not the best solution because it eliminates the power of TS checks

Screenshot from 2019-07-23 12-42-29

@tony

Unfortunately, the react-select type definitions provided by DefinitelyTyped do not export the default OptionType because I believe it can be overridden and defined by the user somehow.

This seems to work , in case someone is looking for something similar to it

import { Props } from "react-select"

type ExtractDefaultType<P> = P extends Props<infer T> ? T : never
type OptionTypeBase = ExtractDefaultType<Props>

tsserver’s hover shows that the extracted type signature is

type OptionTypeBase = {
    label: string;
    value: string;
}

Is there a reason the type cannot simply be { value: string; label: string; } | Array<{ value: string; label: string; } instead of { value: string; label: string; } | OptionsType<{ value: string; label: string; } ?