Typescript -来自联合类型的对象数组的类型定义



我想为一个联合或枚举类型的对象数组创建一个类型定义。如果联合中的所有项不是作为对象数组中的键值存在于数组中,则类型定义失败。

ts操场

export type SelectValue<T = any> = {
value: T;
label: string;
};
type options = "email" | "sms";
//ideally make this type check fail because it does not have all of the values of the options union
const values: SelectValue<options>[]  = [
{
value: "email",
label: "Email",
},
];

TypeScript没有一个内置类型对应于'穷举数组';其中,某种联合类型的每个成员都保证存在于数组的某个位置。您也不能轻松地以这种方式创建自己的特定类型,至少在联合中有很多成员或您希望允许重复的情况下是这样。

对于只有少数成员的联合,如果您想禁止重复,那么您可以生成所有可能接受的元组类型的联合,就像您的示例代码中的[SelectValue<"email">, SelectValue<"sms">] | [SelectValue<"sms">, SelectValue<"email">](见这里)。但这在会员数量𝑛上是非常糟糕的;每个成员包含一个元素的元组的并集本身将具有𝑛!成员(即𝑛! !),它变得非常大,非常快。TypeScript中的union最多只能容纳10万个元素,在此之前,编译器的速度会明显变慢。这意味着如果你的联合中有8个元素,你就会遇到麻烦。


相反,你在TypeScript中可以得到的最接近的是写一个泛型类型,它作为数组类型的约束。即对于并集U,不存在ExhaustiveArray<U>类型;相反,有一个通用的ExhaustiveArray<T, U>类型,其中T extends ExhaustiveArray<T, U>当且仅当T是一个耗尽U所有成员的数组。您还需要一个辅助函数来阻止您自己写出T。也就是说,你应该写const arr = exhaustiveArrayForMyUnion(...)而不是const arr: ExhaustiveArray<MyUnion> = [...]

那么我们来定义这个:

type ExhaustiveArray<T extends readonly any[], U> =
[U] extends [T[number]] ? T : [...T, Exclude<U, T[number]>]
const exhaustiveArray = <U,>() => <T extends readonly U[]>(
...t: [...T extends ExhaustiveArray<T, U> ? T : ExhaustiveArray<T, U>]) => t as T;

这里的ExhaustiveArray<T, U>是一个条件类型,它检查U的并集是否完全由数组T的所有元素的并集构成。如果是,它的计算结果为T(从T extends T开始,这将是成功的)。如果不是,它的计算结果是一个元组,在末尾比T多一个元素,包含所有缺失的元素(使用可变元组类型追加一个元素,并使用Exclude实用程序类型计算缺失的元素)。

exhaustiveArray值是一个柯里化的辅助函数,它接受一个联合类型U,并产生一个新函数,该函数从传入的值推断出T,然后进行检查。它的书写方式很奇怪;如果我们能写<T extends ExhaustiveArray<T, U>>(...t: [...T]) => t就好了,但这是非法的循环。或者如果<T extends readonly U[]>(...t: [...ExhaustiveArray<T, U>]) => t工作,那就太好了,但是编译器将无法从t中推断出T。上面的版本导致编译器首先从t的值推断出T,然后将其转换为ExhaustiveArray<T, U>。这种奇怪方法的全部意义在于得到一个"好"。传入非穷举数组时的错误消息。一个"糟糕的;如果编译器只会说"该值不能赋值给never",这是很烦人的,因为它不能帮助开发人员知道如何修复它。

好的,让我们测试一下。首先让我们获取exhaustiveSelectValueArray()函数:

const exhaustiveSelectValueArray = exhaustiveArray<
{ [K in Options]: SelectValue<K> }[Options]>();

,其中type参数是将Options联合O1 | O2 | O3 | ... | ON转换为SelectValue<O1> | SelectValue<O2> | SelectValue<O3> | ... | SelectValue<ON>的分布式映射类型。

const values = exhaustiveSelectValueArray(
{ value: "email", label: "Email" },
{ value: "sms", label: "SMS" }
); // okay
const err = exhaustiveSelectValueArray(
{ value: "email", label: "Email" }
); // error! Expected 2 arguments, but got 1.
const err2 = exhaustiveSelectValueArray(
{ value: "email", label: "Email" },
{ value: "email", label: "SMS" }
); // error! Expected 3 arguments, but got 2.

第一次调用成功,因为两个元素都传入了,而第二个两次调用失败,因为缺少一个参数。如果你使用智能感知来检查函数调用,看看丢失了什么参数,两者都显示有一个SelectValue<"sms">类型的预期参数没有在最后传入。

Playground链接到代码

最新更新