我想为一个联合或枚举类型的对象数组创建一个类型定义。如果联合中的所有项不是作为对象数组中的键值存在于数组中,则类型定义失败。
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链接到代码