skip to Main Content

I have searched a lot and read several question to find a solution to my problem but in no vain. Can you help me?!

What I don’t understand is that when a type extends boolean and put in the if statement condition it should be narrowed to true but TypeScript has a different idea in some situation:

import Select from "react-select";

export interface Option<Value> {
  readonly value: Value;
  readonly label?: string;
  readonly isDisabled?: boolean;
  readonly isFixed?: boolean;
}

export type PropValue<Value, IsMulti extends boolean> = IsMulti extends true
  ? Value[]
  : Value;

const ComboBox = <Value, IsMulti extends boolean>(props: {
  value?: PropValue<Value, IsMulti>;
  options: Option<Value>[];
  isMulti: IsMulti;
}) => {
  const { value, isMulti, options } = props;
  const mapValue = (x?: PropValue<Value, IsMulti>) => {
    if (!x) return undefined;
    if (isMulti) {
      isMulti;
      // ??? why isMulti is not of type `true` but rather still `extends boolean`
      // ??? x should be an array but doesn't seem to be narrowed as well
      return options.filter(({ value }) => x.includes(value));
    }
  };

  return <Select value={mapValue(value)} isMulti={isMulti} />;
};

A more simple scenario will work as expected:

function experimenting<T extends boolean>(x: boolean, y: T) {
  if (x) {
    x; //: true
  }

  if (y) {
    y; //: true
  }
}
  • Can you explain isMulti in the first scenario didn’t get narrowed to just true?
  • How to fix the code above so that both isMulti and x are narrowed.

2

Answers


  1. T extends boolean doesn’t mean that T is equal to boolean. The extends clause just means that T is a subset of the boolean and the compiler isn’t able to narrow the type to just true since it doesn’t know the exact type of it.

    Example:
    never is an empty set and the subsets of the boolean are true | false | never, since an empty set is also a subset. Thus, we can pass never to the function that expects a generic parameter that extends boolean:

    const func = <T extends boolean>() => {};
    
    type A = never extends boolean ? true : false; // true
    type B = boolean extends never ? true : false; // false
    
    func<never>() // no error
    func<boolean>() // no error
    func<string>() // error
    
    
    Login or Signup to reply.
  2. You can instead of extending Boolean, extend true | undefined on the ComboBox and PropValue, and a default type undefined so that you don’t have to pass isMulti={undefined}
    And in the PropValue you can narrow it down using NonNullable to check if it’s true

    import React from 'react';
    import Select from 'react-select';
    
    export interface Option<Value> {
      readonly value: Value;
      readonly label?: string;
      readonly isDisabled?: boolean;
      readonly isFixed?: boolean;
    }
    
    export type PropValue<
      Value,
      IsMulti extends true | undefined
      //  ^^^^^^^^^^^^^^^^^^^^
    > = IsMulti extends NonNullable<IsMulti> ? Value[] : Value;
    //  ^^^^^^^^^^^^^^^^^^^^^^^^
    
    const ComboBox = <Value, IsMulti extends true | undefined = undefined>(props: {
      //  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      value?: PropValue<Value, IsMulti>;
      options: Option<Value>[];
      isMulti?: IsMulti;
    }) => {
      const { value, isMulti, options } = props;
      const mapValue = (x?: PropValue<Value, IsMulti>) => {
        if (!x) return undefined;
        if (isMulti && Array.isArray(x)) {
                    //  ^^^^^^^^^^^^^^^
          return options.filter(({ value }) => x.includes(value));
        }
      };
    
      return <Select value={mapValue(value)} isMulti={isMulti} />;
    };
    
    export const Test1 = () => {
      return (
        <ComboBox
          value={1}
          options={[
            {
              value: 1,
              label: 'label 1',
            },
            {
              value: 2,
              label: 'label 2',
            },
            {
              value: 3,
              label: 'label 3',
            },
          ]}
        />
      );
    };
    
    export const Test2 = () => {
      return (
        <ComboBox
          isMulti
          value={[1, 2, 3]}
          options={[
            {
              value: 1,
              label: 'label 1',
            },
            {
              value: 2,
              label: 'label 2',
            },
            {
              value: 3,
              label: 'label 3',
            },
          ]}
        />
      );
    };
    

    Also, a union for props would work

    const ComboBox = <Value,>(props: {
      // ...
      value: Value[];
      isMulti: true;
    } | {
      // ...
      value: Value;
      isMulti?: false;
    }) => {
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search