打字稿:如何创建 Array 泛型类型,其中包含具有给定接口的每个 keyof 实例的对象

Posted

技术标签:

【中文标题】打字稿:如何创建 Array 泛型类型,其中包含具有给定接口的每个 keyof 实例的对象【英文标题】:Typescript: How to create Array generic type that includes objects with every instance of keyof given interface 【发布时间】:2021-08-20 00:46:32 【问题描述】:

我有界面

interface Item 
  location: string;
  description: string;

和通用的Field接口

interface Field<T extends object> 
  name: keyof T;
  label: string;

我想要一些Array&lt;Every&lt;T extends object&gt;&gt; 来检查该数组是否包含Item 的每个Field&lt;Item&gt; 的至少一个实例

例子:

错误:

const fields: Every<Field<Item>[]> = [ name: 'description', label: 'Description' ]; //Error: missing Field of "location" instance

正确:

const fields: Every<Field<Item>[]> = [
   name: 'description', label: 'Description' ,
   name: 'location', label: 'location' ,
];

Workaround

【问题讨论】:

【参考方案1】:

TypeScript 没有像这样表示详尽数组的内置功能。您可以尝试编写符合您标准的所有可能的tuples 中的union 类型,但这对于中等规模的联合来说不能很好地扩展,如果你想允许的话,甚至可能对于小情况都难以处理重复条目。

如果我真的想这样做,我会倾向于编写一个帮助函数,它会尝试从数组中推断字段名称,然后使用 conditional type,如果这些字段名称没有,则会导致编译器错误排气keyof T。这可能很脆弱,而且肯定很复杂,但这是一种可能的实现方式:

interface FieldNamed<K extends PropertyKey> 
  name: K,
  label: string


const exhaustiveFieldArray = <T extends object>() => <K extends keyof T>(
  ...fields: [FieldNamed<K>, ...FieldNamed<K>[]] &
    (keyof T extends K ? unknown : FieldNamed<Exclude<keyof T, K>>[])
): Field<T>[] => fields;

const exhaustiveItemFieldArray = exhaustiveFieldArray<Item>();

函数exhaustiveFieldArray&lt;T&gt;() 采用手动指定的类型参数T 并返回一个新函数,该函数接受Field&lt;T&gt; 类型的可变参数数量,如果它不能确定您包含所有字段,则会抱怨名字。

在我们尝试解释它是如何工作的之前,让我们确保它工作:

const fields = exhaustiveItemFieldArray(
   name: 'description', label: 'Description' ,
   name: 'location', label: 'location' 
); // okay

const badFields = exhaustiveItemFieldArray(
   name: 'description', label: 'Description' ,
   name: 'locution', label: 'location'  // error!
  //~~~~ <-- Type '"locution"' is not assignable to type 'keyof Item'
)

const badFields2 = exhaustiveItemFieldArray(
   name: 'description', label: 'Description'  // error!
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Type '"description"' is not assignable to type '"location"'.
)

const badFields3 = exhaustiveItemFieldArray(); // error!
// expected at least one argument    

const badFields4 = exhaustiveItemFieldArray(
   name: 'location', label: 'location' ,
   name: 'location', label: 'location' 
) // error!

我觉得这一切都还好。如果我们缺少字段,我们会得到错误。


这是它如何工作的草图。返回的函数是类型参数K extends keyof T中的generic。 fields rest 参数是两种类型的intersection。第一个用于推断传入的内容:

[FieldNamed<K>, ...FieldNamed<K>[]]

这意味着它是一个至少包含一个元素的数组(它是一个元素的元组,后跟some number of other elements),对于推断的K,每个元素都必须是FieldNamed&lt;K&gt; 类型。这将最终使 K 包含所有字段名称的并集。

第二种类型用于检查K 是否详尽无遗keyof T。我们已经知道K extends keyof T,但我们也想确保keyof T extends K

& (keyof T extends K ? unknown : FieldNamed<Exclude<keyof T, K>>[])

此条件类型检查keyof T extends K。如果是真的,那么一切都很好,我们返回unknown。与unknown 相交是无操作(XYZ &amp; unknown 等效于XYZ),因此不会阻止任何东西的编译。如果为假,那么我们有问题,我们返回FieldNamed&lt;Exclude&lt;keyof T, K&gt;&gt;[]。 The Exclude&lt;T, U&gt; utility type 从联合中移除元素;所以Exclude&lt;keyof T, K&gt; 为我们提供了我们遗漏的那些键。因此,我们将实际数组类型与我们错过的字段数组相交。这将导致编译器错误抱怨你错过了一些事情。这些错误可能不是最容易理解的,但至少有错误。

所以,万岁?它的工作原理,但我不知道它是否值得你。相反,您可以考虑将数据结构从数组(编译器难以检查)更改为键与 T 相同的对象(编译器易于检查)。但是这个答案已经很长了,所以我不打算进一步扩大范围来展示如何实现这样的事情。 ?

Playground link to code

【讨论】:

酷!这对我的使用来说已经足够了。没想到回答这么详细。谢谢你。我们有很多已定义的数组。所以我更喜欢创建一些数组类型而不是将所有代码重写为字典。

以上是关于打字稿:如何创建 Array 泛型类型,其中包含具有给定接口的每个 keyof 实例的对象的主要内容,如果未能解决你的问题,请参考以下文章

如何将方法的泛型类型限制为打字稿中的对象?

打字稿:允许泛型类型仅是具有“字符串”属性的对象

如何在打字稿的泛型类中创建类型为“T”的新对象?

如何检查泛型 K 是不是属于泛型 M |打字稿通用 |还原

打字稿承诺泛型类型

打字稿泛型类型 T toString