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

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

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:

  1. import Select from "react-select";
  2. export interface Option<Value> {
  3. readonly value: Value;
  4. readonly label?: string;
  5. readonly isDisabled?: boolean;
  6. readonly isFixed?: boolean;
  7. }
  8. export type PropValue<Value, IsMulti extends boolean> = IsMulti extends true
  9. ? Value[]
  10. : Value;
  11. const ComboBox = <Value, IsMulti extends boolean>(props: {
  12. value?: PropValue<Value, IsMulti>;
  13. options: Option<Value>[];
  14. isMulti: IsMulti;
  15. }) => {
  16. const { value, isMulti, options } = props;
  17. const mapValue = (x?: PropValue<Value, IsMulti>) => {
  18. if (!x) return undefined;
  19. if (isMulti) {
  20. isMulti;
  21. // ??? why isMulti is not of type `true` but rather still `extends boolean`
  22. // ??? x should be an array but doesn't seem to be narrowed as well
  23. return options.filter(({ value }) => x.includes(value));
  24. }
  25. };
  26. return <Select value={mapValue(value)} isMulti={isMulti} />;
  27. };

A more simple scenario will work as expected:

  1. function experimenting<T extends boolean>(x: boolean, y: T) {
  2. if (x) {
  3. x; //: true
  4. }
  5. if (y) {
  6. y; //: true
  7. }
  8. }
  • 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:

  1. import Select from &quot;react-select&quot;;
  2. export interface Option&lt;Value&gt; {
  3. readonly value: Value;
  4. readonly label?: string;
  5. readonly isDisabled?: boolean;
  6. readonly isFixed?: boolean;
  7. }
  8. export type PropValue&lt;Value, IsMulti extends boolean&gt; = IsMulti extends true
  9. ? Value[]
  10. : Value;
  11. const ComboBox = &lt;Value, IsMulti extends boolean&gt;(props: {
  12. value?: PropValue&lt;Value, IsMulti&gt;;
  13. options: Option&lt;Value&gt;[];
  14. isMulti: IsMulti;
  15. }) =&gt; {
  16. const { value, isMulti, options } = props;
  17. const mapValue = (x?: PropValue&lt;Value, IsMulti&gt;) =&gt; {
  18. if (!x) return undefined;
  19. if (isMulti) {
  20. isMulti;
  21. // ??? why isMulti is not of type `true` but rather still `extends boolean`
  22. // ??? x should be an array but doesn&#39;t seem to be narrowed as well
  23. return options.filter(({ value }) =&gt; x.includes(value));
  24. }
  25. };
  26. return &lt;Select value={mapValue(value)} isMulti={isMulti} /&gt;;
  27. };

A more simple scenario will work as expected:

  1. function experimenting&lt;T extends boolean&gt;(x: boolean, y: T) {
  2. if (x) {
  3. x; //: true
  4. }
  5. if (y) {
  6. y; //: true
  7. }
  8. }
  • 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 的函数:

  1. const func = <T extends boolean>() => {};
  2. type A = never extends boolean ? true : false; // true
  3. type B = boolean extends never ? true : false; // false
  4. func<never>() // 没有错误
  5. func<boolean>() // 没有错误
  6. 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:

  1. const func = &lt;T extends boolean&gt;() =&gt; {};
  2. type A = never extends boolean ? true : false; // true
  3. type B = boolean extends never ? true : false; // false
  4. func&lt;never&gt;() // no error
  5. func&lt;boolean&gt;() // no error
  6. func&lt;string&gt;() // error

答案2

得分: 0

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

  1. 您可以改为在`ComboBox``PropValue`上扩展`true | undefined`,而不是扩展`Boolean`,并使用默认类型`undefined`,这样您就不必传递`isMulti={undefined}`
  2. 并且在`PropValue`中,您可以使用`NonNullable`来缩小范围,以检查它是否为`true`
  3. ```tsx
  4. import React from 'react';
  5. import Select from 'react-select';
  6. export interface Option<Value> {
  7. readonly value: Value;
  8. readonly label?: string;
  9. readonly isDisabled?: boolean;
  10. readonly isFixed?: boolean;
  11. }
  12. export type PropValue<
  13. Value,
  14. IsMulti extends true | undefined
  15. // ^^^^^^^^^^^^^^^^^^^^
  16. > = IsMulti extends NonNullable<IsMulti> ? Value[] : Value;
  17. // ^^^^^^^^^^^^^^^^^^^^^^^^
  18. const ComboBox = <Value, IsMulti extends true | undefined = undefined>(props: {
  19. // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  20. value?: PropValue<Value, IsMulti>;
  21. options: Option<Value>[];
  22. isMulti?: IsMulti;
  23. }) => {
  24. const { value, isMulti, options } = props;
  25. const mapValue = (x?: PropValue<Value, IsMulti>) => {
  26. if (!x) return undefined;
  27. if (isMulti && Array.isArray(x)) {
  28. // ^^^^^^^^^^^^^^^
  29. return options.filter(({ value }) => x.includes(value));
  30. }
  31. };
  32. return <Select value={mapValue(value)} isMulti={isMulti} />;
  33. };
  34. export const Test1 = () => {
  35. return (
  36. <ComboBox
  37. value={1}
  38. options={[
  39. {
  40. value: 1,
  41. label: 'label 1',
  42. },
  43. {
  44. value: 2,
  45. label: 'label 2',
  46. },
  47. {
  48. value: 3,
  49. label: 'label 3',
  50. },
  51. ]}
  52. />
  53. );
  54. };
  55. export const Test2 = () => {
  56. return (
  57. <ComboBox
  58. isMulti
  59. value={[1, 2, 3]}
  60. options={[
  61. {
  62. value: 1,
  63. label: 'label 1',
  64. },
  65. {
  66. value: 2,
  67. label: 'label 2',
  68. },
  69. {
  70. value: 3,
  71. label: 'label 3',
  72. },
  73. ]}
  74. />
  75. );
  76. };

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

  1. const ComboBox = <Value>(props: {
  2. // ...
  3. value: Value[];
  4. isMulti: true;
  5. } | {
  6. // ...
  7. value: Value;
  8. isMulti?: false;
  9. }) => {
  1. <details>
  2. <summary>英文:</summary>
  3. 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}`
  4. And in the `PropValue` you can narrow it down using `NonNullable` to check if it&#39;s `true`
  5. ```tsx
  6. import React from &#39;react&#39;;
  7. import Select from &#39;react-select&#39;;
  8. export interface Option&lt;Value&gt; {
  9. readonly value: Value;
  10. readonly label?: string;
  11. readonly isDisabled?: boolean;
  12. readonly isFixed?: boolean;
  13. }
  14. export type PropValue&lt;
  15. Value,
  16. IsMulti extends true | undefined
  17. // ^^^^^^^^^^^^^^^^^^^^
  18. &gt; = IsMulti extends NonNullable&lt;IsMulti&gt; ? Value[] : Value;
  19. // ^^^^^^^^^^^^^^^^^^^^^^^^
  20. const ComboBox = &lt;Value, IsMulti extends true | undefined = undefined&gt;(props: {
  21. // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  22. value?: PropValue&lt;Value, IsMulti&gt;;
  23. options: Option&lt;Value&gt;[];
  24. isMulti?: IsMulti;
  25. }) =&gt; {
  26. const { value, isMulti, options } = props;
  27. const mapValue = (x?: PropValue&lt;Value, IsMulti&gt;) =&gt; {
  28. if (!x) return undefined;
  29. if (isMulti &amp;&amp; Array.isArray(x)) {
  30. // ^^^^^^^^^^^^^^^
  31. return options.filter(({ value }) =&gt; x.includes(value));
  32. }
  33. };
  34. return &lt;Select value={mapValue(value)} isMulti={isMulti} /&gt;;
  35. };
  36. export const Test1 = () =&gt; {
  37. return (
  38. &lt;ComboBox
  39. value={1}
  40. options={[
  41. {
  42. value: 1,
  43. label: &#39;label 1&#39;,
  44. },
  45. {
  46. value: 2,
  47. label: &#39;label 2&#39;,
  48. },
  49. {
  50. value: 3,
  51. label: &#39;label 3&#39;,
  52. },
  53. ]}
  54. /&gt;
  55. );
  56. };
  57. export const Test2 = () =&gt; {
  58. return (
  59. &lt;ComboBox
  60. isMulti
  61. value={[1, 2, 3]}
  62. options={[
  63. {
  64. value: 1,
  65. label: &#39;label 1&#39;,
  66. },
  67. {
  68. value: 2,
  69. label: &#39;label 2&#39;,
  70. },
  71. {
  72. value: 3,
  73. label: &#39;label 3&#39;,
  74. },
  75. ]}
  76. /&gt;
  77. );
  78. };

Also, a union for props would work

  1. const ComboBox = &lt;Value,&gt;(props: {
  2. // ...
  3. value: Value[];
  4. isMulti: true;
  5. } | {
  6. // ...
  7. value: Value;
  8. isMulti?: false;
  9. }) =&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:

确定