【问题标题】:Typescript: How to create Array generic type that includes objects with every instance of keyof given interface打字稿:如何创建 Array 泛型类型,其中包含具有给定接口的每个 keyof 实例的对象
【发布时间】: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

【问题讨论】:

    标签: arrays typescript generics


    【解决方案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中的genericfields 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

    【讨论】:

    • 酷!这对我的使用来说已经足够了。没想到回答这么详细。谢谢你。我们有很多已定义的数组。所以我更喜欢创建一些数组类型而不是将所有代码重写为字典。
    猜你喜欢
    • 1970-01-01
    • 2022-08-21
    • 2015-08-25
    • 2019-01-16
    • 2022-01-18
    • 2012-10-08
    • 2017-12-14
    • 2017-06-11
    • 2020-10-11
    相关资源
    最近更新 更多