打字稿:如何创建 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<Every<T extends object>>
来检查该数组是否包含Item
的每个Field<Item>
的至少一个实例
例子:
错误:
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<T>()
采用手动指定的类型参数T
并返回一个新函数,该函数接受Field<T>
类型的可变参数数量,如果它不能确定您包含所有字段,则会抱怨名字。
在我们尝试解释它是如何工作的之前,让我们确保它工作:
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<K>
类型。这将最终使 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 & unknown
等效于XYZ
),因此不会阻止任何东西的编译。如果为假,那么我们有问题,我们返回FieldNamed<Exclude<keyof T, K>>[]
。 The Exclude<T, U>
utility type 从联合中移除元素;所以Exclude<keyof T, K>
为我们提供了我们遗漏的那些键。因此,我们将实际数组类型与我们错过的字段数组相交。这将导致编译器错误抱怨你错过了一些事情。这些错误可能不是最容易理解的,但至少有错误。
所以,万岁?它的工作原理,但我不知道它是否值得你。相反,您可以考虑将数据结构从数组(编译器难以检查)更改为键与 T
相同的对象(编译器易于检查)。但是这个答案已经很长了,所以我不打算进一步扩大范围来展示如何实现这样的事情。 ?
Playground link to code
【讨论】:
酷!这对我的使用来说已经足够了。没想到回答这么详细。谢谢你。我们有很多已定义的数组。所以我更喜欢创建一些数组类型而不是将所有代码重写为字典。以上是关于打字稿:如何创建 Array 泛型类型,其中包含具有给定接口的每个 keyof 实例的对象的主要内容,如果未能解决你的问题,请参考以下文章