0
votes

I'm using Elastic UI component Dual range:

const [yearRangeVal, setYearRangeVal] = useState([1974, 2007]);
  
const onYearRangeChange = (value: <number[]>) => {
    setYearRangeVal(value);
  };

      <EuiDualRange
        min={1974}
        max={2007}
        showTicks
        tickInterval={10}
        onChange={onYearRangeChange}
        value={yearRangeVal}
      />

And getting this error in VSCode:

No overload matches this call. Overload 2 of 2, '(props: EuiDualRangeProps, context: any): EuiDualRange', gave the following error. Type '(value: <number[]>) => void' is not assignable to type '(values: [ReactText, ReactText], isValid: boolean, event: ChangeEvent | MouseEvent<HTMLButtonElement, MouseEvent> | KeyboardEvent<...>) => void'. Overload 2 of 2, '(props: EuiDualRangeProps, context: any): EuiDualRange', gave the following error. Type 'number[]' is not assignable to type '[ReactText, ReactText]'.

This code is from Elastic UI library (see ValueMember variable):

type ValueMember = number | string;
    export interface EuiDualRangeProps extends Omit<EuiRangeSliderProps, 'onChange' | 'onBlur' | 'onFocus' | 'value'> {
        value: [ValueMember, ValueMember];
        onBlur?: (event: React.FocusEvent<HTMLInputElement> | React.FocusEvent<HTMLDivElement>) => void;
        onFocus?: (event: React.FocusEvent<HTMLInputElement> | React.FocusEvent<HTMLDivElement>) => void;
        onChange: (values: [ValueMember, ValueMember], isValid: boolean, event: React.ChangeEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLInputElement>) => void;
    

Just can't unerstand what is ReactText ((JSX attribute) value: [React.ReactText, React.ReactText]) and why my types are not ok.

1
Try useState<[number, number]>([1974, 2007]). If I recall correctly (on mobile and unable to check) ReactText includes number. The issue is that it specifically wants an array of length 2 (a tuple). But typescript will assume that your state can be a number array of any length unless you specifically tell it otherwise. - Linda Paiste
@LindaPaiste , thanks for the response! But it didn't help: Type '[ReactText, ReactText]' is not assignable to type 'SetStateAction<[number, number]>'. Type '[ReactText, ReactText]' is not assignable to type '[number, number]'. - Богдан

1 Answers

5
votes

ReactText is an alias for string | number. It defines what types can be printed out as texts in React, which is both strings and numbers.

The EuiDualRange component is saying that its value must always be an array with two elements aka a Tuple type. Even though your state array is initialized with only two values, [1974, 2007], typescript assumes that it could be any length and would allow you to setState with a number array of any length. In other other words, the inferred type for useState is number[] but we want to limit it to only [number, number].

When you manually set the generic of useState with useState<[number, number]>([1974, 2007]) then the error on the property value goes away.

But we will still have an error on onChange, even if we set (value: [number, number]). This is because the EuiDualRange can accept string AND number and the typescript definition for its onChange callback says that the value it returns could be strings or numbers, so it expects your callback to accept both.

One way to make sure that your callback accepts the right types is to import the definition from the package, which is for the whole function rather than the props, and apply it to your callback.

import {EuiDualRange, EuiDualRangeProps} from "@elastic/eui";
const onYearRangeChange: EuiDualRangeProps['onChange'] = (value) => { /** ... **/ }

If we do that, our callback now accepts the right type of value, but we get an error further down when calling setState with that value because the value type is broader than our state type.

There are many ways to address this.

  1. If we can be confident based on the behavior of the package that the component will always call onChange with the same type as what we gave it through value then we can basically tell typescript "ignore the error because I know more than you". You would write
  const onYearRangeChange: EuiDualRangeProps['onChange'] = (value) => {
    setYearRangeVal(value as [number, number]);
  };

Where the "as" keyword is used to refine the type (type assertion). This is the simplest solution. Some people avoid this strategy because you open yourself up to potential errors if it turns out that you were wrong about what you were asserting.

Based on what I'm seeing from the EuiDualRange component, it is in fact returning numbers so I think it is fine to use "as" in this case. But I want to include an alternative because this might not be "best practice".

  1. You can narrow the type by checking the returned values and making sure that they actually are numbers and not strings (type guards). This means that your code is doing extra work at runtime (though it's minuscule), but you get absolute certainty that your types are correct. In the case that types are wrong, you can do nothing or you could throw an error. In this example I'm destructuring the tuple to its individual elements for readability.
  const onYearRangeChange: EuiDualRangeProps['onChange'] = ([min, max]) => {
    if ( typeof min === "number" && typeof max === "number") {
      setYearRangeVal([min, max]);
    }
    // optional else { }
  };
  1. If it's not important to your code whether the years are numbers or strings, you could allow your useState to accept both numbers and strings.
const [yearRangeVal, setYearRangeVal] = useState<[ReactText, ReactText]>([1974, 2007]);