【问题标题】:Convert array of types to union of object type将类型数组转换为对象类型的并集
【发布时间】:2021-04-24 00:21:24
【问题描述】:

假设我有这些类型:

type StepA = // ...
type StepB = // ...
type StepC = // ...

type Steps = [StepA, StepB, StepC]

我想要一个实用程序类型SomeUtilityType<T> 以便:

type StepsCombinations = SomeUtilityType<Steps>
// type StepsCombinations =
//   | { step: 0; value: StepA }
//   | { step: 1; value: StepB }
//   | { step: 2; value: StepC };

我应该如何定义SomeUtilityType&lt;T&gt;

编辑

如果Steps被定义为一个对象

type Steps = {0: StepA, 1: StepB, 2: StepC}

这个使用Distributive Conditional Types 的实用程序可以解决问题(参见playground):

type SomeUtilityType<
  V extends { [index: number]: unknown },
  T extends keyof V = keyof V
> = T extends unknown
  ? { step: T; value: V[T]; }
  : never;

但我更希望避免使用多余的键 0, 1 , 2... 定义对象。

我想我可以使用上面的 SomeUtilityType 类型定义,如果有某种方法可以获得数组中所有索引的联合,例如 Indexes&lt;Steps&gt; // --&gt; 0 | 1 | 2

Playground Link

【问题讨论】:

  • Steps 可能存在多长时间? TypeScript 倾向于提供像 "0" 这样的元组键字符串表示,而不是 0。如果你想要{step: "2", value: StepC},那很好。如果您需要将"2" 转换为2,那么我们需要编写一些东西来做到这一点,这并不简单。如果您只需要支持最大有限长度的元组,那么我们可以考虑到这一点来编写实用程序类型,而不是尝试在类型系统中表示“将字符串转换为等效数字”。
  • 步数会很短,比如说少于 10 个位置。我同意"2" 而不是2。顺便说一句,我刚刚用我找到的一些替代解决方案编辑了我的问题,但仍然不是最理想的。
  • 好的,看我的回答。

标签: typescript


【解决方案1】:

您可以使用mapped tuple types 来获得您想要的大部分内容。唯一棘手的部分是,无论出于何种原因,元组类型的已知键都是string literal types,如"0""123",而不是number,如0123。并且没有将前者转换为后者的 StrToNum&lt;T&gt; 实用程序类型。我已经commented before 关于希望这些东西能够以编程方式处理元组。哦,好吧。

如果您可以使用字符串文字而不是数字文字,那么这很简单:

type SomeUtilityType<T extends readonly any[]> =
  { [K in keyof T]: { step: K, value: T[K] } }[number];

type StepsCombinations = SomeUtilityType<Steps>;
/* type StepsCombinations = {
    step: "0";
    value: StepA;
} | {
    step: "1";
    value: StepB;
} | {
    step: "2";
    value: StepC;
} */

另一方面,如果我们想得到{step: 0, value: StepA} 而不是{step: "0", value: StepA},我们需要自己进行转换。这是我可能会这样做的方式:

// only accepts steps up to 99, but you can extend if you want
type Nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
  12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
  23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 
  34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
  46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 
  58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68,
  69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 
  80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
  92, 93, 94, 95, 96, 97, 98, 99];

type SomeUtilityType<T extends readonly any[]> =
  { [K in keyof T]: { step: Nums[Extract<K, keyof Nums>], value: T[K] } }[number];

这使用硬编码的Nums 元组将字符串转换为数字。它停在99,但您可以根据需要扩展它。您可以验证 StepsCombinations 是您想要的输出:

type StepsCombinations = SomeUtilityType<Steps>
/* type StepsCombinations = {
    step: 0;
    value: StepA;
} | {
    step: 1;
    value: StepB;
} | {
    step: 2;
    value: StepC;
} */

您可能对硬编码列表不满意。好吧,您可以使用 recursive utility types 编写一个不同的版本来执行概念上适用于任意长元组的操作:

type SomeOtherUtilityType<T extends readonly any[], U = never> =
  T extends readonly [...infer I, infer L] ?
  SomeOtherUtilityType<I, { step: I['length'], value: L } | U> : U;

并验证它是否适用于您的示例:

type StepsCombinations2 = SomeOtherUtilityType<Steps>
/* type StepsCombinations2 = {
    step: 2;
    value: StepC;
} | {
    step: 1;
    value: StepB;
} | {
    step: 0;
    value: StepA;
} */

(是的,即使编译器以不同的顺序编写联合,它们也是相同的类型)。

不幸的是,硬编码列表版本表现更好。 TypeScript 有相当浅的递归限制,所以如果你传入一个长度超过 50 的元组,递归版本就会中断:

type Okay = SomeUtilityType<Nums> // fine
type Oops = SomeOtherUtilityType<Nums> // error!
// Type instantiation is excessively deep and possibly infinite.

因此,如果您需要数字文字,我仍然推荐使用 Nums 的版本。

Playground link to code

【讨论】:

    猜你喜欢
    • 2021-09-09
    • 1970-01-01
    • 1970-01-01
    • 2021-08-11
    • 2018-11-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多