TypeScript中的数组元编程
大多数 TypeScript 开发人员很容易注意到他们可以使用通用类型对数组进行建模。你有没有想过,你是否可以推断出一个数组的类型?或者,如何根据其类型获得所有数组元素?
如果你不知道如何做到这一点,请继续阅读!
如果你是一个更高级的TypeScript开发者,我也为你准备了一些值得你花时间的东西!
从一个数组推断出数组类型
推断一个特定数组的数组类型是更高级的元编程的基础。如果你是一个初学者(这没有错,每个人都是从头开始的!),你可能会尝试用以下方式来做。
这就成功了,因为array1 的类型是string[] 。因为你在得到它的类型后仍然将元素推送到array1 ,TypeScript必须推断它是一个可变数组。从它的初始元素来看,是一个可变的字符串数组。
如果我们想有一个更精确的类型呢?我们需要在不使用ReadonlyArray<T> 类型的情况下使有关数组成为不可变的--否则,我们就需要指定数组类型,这没有任何意义。我们应该使用as const 表示。
array2 的类型是readonly ['a' | 'b' | 'c'] 。我们实现了我们想要的东西,然而,TypeScript允许我们也提取数组元素的类型!这需要使用 关键字。这需要以如下方式使用infer 关键字。
InferRAET ("推断只读数组元素类型")是一个有条件的通用类型。它接受一个类型A ,只检查它是否是一个ReadonlyArray 类型。
我们用infer I 来标记推断的元素类型。如果A 是一个ReadonlyArray 类型,推断出的元素类型会被返回。否则,我们返回never 类型。
在使用InferREAT ,ET (元素类型)等于'a' | 'b' | 'c' ,它是一个联合类型。
如果你认为使用InferRAET ,实在是太复杂了,我有个好消息。存在一个更简洁的解决方案,它使用索引访问类型。
ET equals ,因为数组使用数字进行索引。ET2
条件类型的分布性
如果我们想把一个只读的数组类型变成一个可写的数组类型呢?这种转换似乎可以用一句话来实现,如下图所示。
TransformRA2WA 类型将只读数组转换为可写数组。正如你所看到的,类型RA1 和RA2 是不同的,它们转换后的类型WA1 和WA2 也是不同的。
为了检查类型是否相等,我使用了ts-essentials 库中的Exact 类型。E1 和E2 都等于never ,这意味着被测试的类型是不完全的。
TypeScript在处理条件类型时,在通用联合类型上应用分布。这就是前面片段中缺乏类型平等的原因。这个语言特性在这里有很好的描述。
如果我们想在我们的例子中禁用分布,我们需要做以下工作。
正如你所看到的,TransformRA2WAND ("将只读数组转化为可写数组,不分配 "的简化名称)与TransformRA2WA ,只有[] (方括号)围绕在extends表达式的左右两边。如前所述,WA3 在类型上等同于WA4 。
来自一个数组类型的所有数组元素
如果你曾经试图进行转换,你知道这不是一件容易的事。它可以归结为将联合类型转换为元组类型--不幸的是,不存在TypeScript操作符来支持这个。如果你想深入了解这个话题,你可以访问这个GitHub问题。
同时,我将描述这个问题的以下三种解决方案。
解决方案1
由于联盟类型与元组类型之间的转换仍然很复杂,我们可以将联盟解构为对象的属性。这是由joaopaulobdac在他们的评论中发现的,虽然我不知道他们是否是第一个发现这个方法的人。我们可以很容易地在TypeScript中键入这样一个对象,如下面的片段所示。
你可能会问,为什么ET3KeyToOneDictionary 中的值总是被设置为1 。它几乎可以被设置为任何值,但我希望有一个单字符值,以节省打字。正如你可能看到的,所提出的方法有两个问题。
- 构建
et3KeyToOneDictionary可能很麻烦。 allElements属于类型 ,而不是类型 。string[]ET3[]
解决方案2
由于第一个解决方案并不简明,我们需要另一个解决方案。由于vojtechhabarta发现我们可以在这里利用枚举,所以我对第一个解决方案进行了相应的调整。下面的片段就是结果了。
这段代码绝对值得解释一下。这个名为ET4KeyToNumberDictionary 的字典类型将联合类型的键映射成数字。我这样做是为了确保ET4KeyToNumberDictionary 与下面的Enum4 类型相匹配。提取所有的枚举键并不容易(你可能想自己检查一下为什么我必须过滤这些键)。
由于TypeScript中的枚举不能明确地扩展类型,我需要自己确保类型安全。为此,我写了一个类型保护,它只有两种状态--0 或1 。后者只在Enum4 在类型级别上扩展ET4KeyToNumberDictionary 时发生。之后,我设置typeGuard ,将1 作为一个值。
只要类型是正确的,代码就会被编译。你可能想知道为什么我在其定义后调用typeGuard 。我这样做是为了确保编译器对未使用的常量的遵守。
解决方案3
karol-majewski提出了到目前为止我所看到的最好的解决方案。尽管这是一个非常高级的片段,但我还是要一步一步地教你。
ValueOf类型
首先,我们需要讨论ValueOf 通用类型。由于很多TypeScript开发者使用它,一些库已经包含它,ts-essentials 是最明显的例子。正式的定义看起来如下。
如果我们在一个数组类型上使用ValueOf ,会发生什么?
由于数组是对象,我们得到的是该对象中所有值的联合类型,特别是数组方法和所有数组元素类型。
ValueOf类型和Extends关键字
其次,我必须解释一下在MustInclude ,在我们正在讨论的例子中详细说明的类型的内部情况。我将使用下面的片段来解构那里发生的事情。
T1 是一个联合类型,比元组类型有更多的类型,叫做 。正因为如此, 延伸了 ,同样, 延伸了 。一开始听起来可能有悖常理,但是 这个关键字使得编译器只检查 是否可以分配给 。你可以U1 U1 T1[] 'a' string extends a string在这里阅读更多的内容。
上一段解释了为什么C1 等于1 。那么C2 呢?C2 是否也等于1 ?
不是的!
如果U1 是一个字符串字元的元组,那么ValueOf<U1> 是什么?对于这个表达式,我们可以把U1 作为一个字符串字数的数组类型。这样的数组在JavaScript中始终是一个对象。
如前所述,ValueOf 类型应用于一个对象类型的结果是所有属性值类型的联合。对于一个数组类型,这可以归结为一个联合。
- 大量的函数类型。
- 阵列元素类型。
U1 的数组元素类型是什么?它一定是'a','b' 和'c' 。我们可以把T1 至少分配给这些元素类型的联盟吗?显然不能,因为它们不包含'd' 的字符串字面类型。
这就证明了为什么T1 没有扩展到ValueOf<U1> ,因此,C2 等于0 。
作为一个练习,你可以把'c' | 'd' 从T1 ,并重新思考之前所概述的推理。
必须包括的类型和分布
第三,热心的读者一定认识到我已经解释了关于karol-majewski所提供的解决方案的几乎所有内容。几乎所有的东西,因为MustInclude 类型使用了非分布式的条件类型,用方括号包围着extends 表达式的每一边表示。
正如你可能意识到的,这不是错误,分布必须被禁用。
MustInclude 有两个通用类型,但只有第一个( )是一个联合类型。这意味着在分布的情况下, 表达式将为每个联合元素被调用,而结束类型将是所有这些表达式结果的联合。T T extends ValueOf<U>
我们可以应用这种思维来编写一个模仿这种行为的代码段。
尽管根据我们对前一个片段的了解,T1 并没有扩展ValueOf<U1> ,但所有extends 表达式的联合 (C) 等于U1 。这只是因为应用于任何联合类型的never 类型并不改变它。
所有元组元素到一个数组类型和数组元素
由于元组在某些情况下可以用数组来近似,我们可以很容易地从元组类型中获得所有元素,并将它们转换为数组类型。请看下面的片段。
我们只需复制元组定义并将其设置为一个值,就可以提取所有元组元素。为了推断元组内元素的联合类型,我们需要使用之前定义的通用类型InferREAT 。这就是TypeScript将元组转换为一个(只读)数组进行推断的时刻。在我们提取了元素类型ETT1[] ,我们可以定义数组类型。
用io-ts进行元编程
io-ts 库允许软件开发者为JSON对象的反序列化创建编解码器,还有许多其他用途。它支持许多元编程模式,特别是在处理数组类型的时候。我写了下面这个片段,向你展示它的能力的一个例子。
codec 结构在其内部编码了(不是双关语)类型数据,我们可以使用TypeOf 类型提取它们--我建议更高级的TypeScript开发者阅读管理这种类型提取的代码。这绝对值得你花时间去完全掌握它,并可能在你自己的项目中使用相同的模式。
我们还可以使用所提供的编解码器接口来获得所有数组元素(allElementsFromCodec)。通过定义一个编解码器,我们可以自动获得数组类型和它的所有元素。
总结
数组元编程仍然是TypeScript中一个广泛的话题。到目前为止,我们可以很容易地将数组类型与数组元素联系起来,并执行从一个数组中推断出的操作,反之亦然。我们还观察到,在特定场景下,数组可以与图元互换使用。