Complex conditional types in typescript, narrowing, and `extends` keyword.

huangapple go评论68阅读模式
英文:

Complex conditional types in typescript, narrowing, and `extends` keyword

问题

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.
英文:

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 &quot;react-select&quot;;

export interface Option&lt;Value&gt; {
  readonly value: Value;
  readonly label?: string;
  readonly isDisabled?: boolean;
  readonly isFixed?: boolean;
}

export type PropValue&lt;Value, IsMulti extends boolean&gt; = IsMulti extends true
  ? Value[]
  : Value;

const ComboBox = &lt;Value, IsMulti extends boolean&gt;(props: {
  value?: PropValue&lt;Value, IsMulti&gt;;
  options: Option&lt;Value&gt;[];
  isMulti: IsMulti;
}) =&gt; {
  const { value, isMulti, options } = props;
  const mapValue = (x?: PropValue&lt;Value, IsMulti&gt;) =&gt; {
    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&#39;t seem to be narrowed as well
      return options.filter(({ value }) =&gt; x.includes(value));
    }
  };

  return &lt;Select value={mapValue(value)} isMulti={isMulti} /&gt;;
};

A more simple scenario will work as expected:

function experimenting&lt;T extends boolean&gt;(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.

答案1

得分: 2

T extends boolean 并不意味着 T 等于 booleanextends 子句只表示 Tboolean 的子集,而编译器无法将类型缩小到仅仅是 true,因为它不知道它的确切类型。

例如:
never 是一个空集,而 boolean 的子集包括 true | false | never,因为空集也是一个子集。因此,我们可以将 never 传递给期望泛型参数扩展为 boolean 的函数:

const func = <T extends boolean>() => {};

type A = never extends boolean ? true : false; // true
type B = boolean extends never ? true : false; // false

func<never>() // 没有错误
func<boolean>() // 没有错误
func<string>() // 错误
英文:

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 = &lt;T extends boolean&gt;() =&gt; {};

type A = never extends boolean ? true : false; // true
type B = boolean extends never ? true : false; // false

func&lt;never&gt;() // no error
func&lt;boolean&gt;() // no error
func&lt;string&gt;() // error

答案2

得分: 0

You can translate the code comments and type annotations as follows:

您可以改为在`ComboBox``PropValue`上扩展`true | undefined`,而不是扩展`Boolean`,并使用默认类型`undefined`,这样您就不必传递`isMulti={undefined}`
并且在`PropValue`中,您可以使用`NonNullable`来缩小范围,以检查它是否为`true`

```tsx
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',
        },
      ]}
    />
  );
};

此外,用于props的联合类型也可以使用

const ComboBox = <Value>(props: {
  // ...
  value: Value[];
  isMulti: true;
} | {
  // ...
  value: Value;
  isMulti?: false;
}) => {

<details>
<summary>英文:</summary>
You can instead of extending `Boolean`, extend `true | undefined` on the `ComboBox` and `PropValue`, and a default type `undefined` so that you don&#39;t have to pass `isMulti={undefined}`
And in the `PropValue` you can narrow it down using `NonNullable` to check if it&#39;s `true`
```tsx
import React from &#39;react&#39;;
import Select from &#39;react-select&#39;;
export interface Option&lt;Value&gt; {
readonly value: Value;
readonly label?: string;
readonly isDisabled?: boolean;
readonly isFixed?: boolean;
}
export type PropValue&lt;
Value,
IsMulti extends true | undefined
//  ^^^^^^^^^^^^^^^^^^^^
&gt; = IsMulti extends NonNullable&lt;IsMulti&gt; ? Value[] : Value;
//  ^^^^^^^^^^^^^^^^^^^^^^^^
const ComboBox = &lt;Value, IsMulti extends true | undefined = undefined&gt;(props: {
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
value?: PropValue&lt;Value, IsMulti&gt;;
options: Option&lt;Value&gt;[];
isMulti?: IsMulti;
}) =&gt; {
const { value, isMulti, options } = props;
const mapValue = (x?: PropValue&lt;Value, IsMulti&gt;) =&gt; {
if (!x) return undefined;
if (isMulti &amp;&amp; Array.isArray(x)) {
//  ^^^^^^^^^^^^^^^
return options.filter(({ value }) =&gt; x.includes(value));
}
};
return &lt;Select value={mapValue(value)} isMulti={isMulti} /&gt;;
};
export const Test1 = () =&gt; {
return (
&lt;ComboBox
value={1}
options={[
{
value: 1,
label: &#39;label 1&#39;,
},
{
value: 2,
label: &#39;label 2&#39;,
},
{
value: 3,
label: &#39;label 3&#39;,
},
]}
/&gt;
);
};
export const Test2 = () =&gt; {
return (
&lt;ComboBox
isMulti
value={[1, 2, 3]}
options={[
{
value: 1,
label: &#39;label 1&#39;,
},
{
value: 2,
label: &#39;label 2&#39;,
},
{
value: 3,
label: &#39;label 3&#39;,
},
]}
/&gt;
);
};

Also, a union for props would work

const ComboBox = &lt;Value,&gt;(props: {
  // ...
  value: Value[];
  isMulti: true;
} | {
  // ...
  value: Value;
  isMulti?: false;
}) =&gt; {

huangapple
  • 本文由 发表于 2023年7月4日 21:31:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/76613171.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定