【问题标题】:Why does type Record sometimes complain about missing keys and sometimes not?为什么键入 Record 有时会抱怨缺少键,有时不会?
【发布时间】:2021-07-23 06:24:00
【问题描述】:

考虑以下代码:

const testOne: Record<"foo"|"bar", string> = {
    "foo": "xyz"
};
const testTwo: Record<string, string> = {
    "foo": "xyz"
};

第一个示例导致缺少属性“bar”的错误。第二个示例不会导致错误。这让我感到困惑,因为我试图了解 Record 是否是一种暗示其键类型的所有可能值的现有属性的类型。

如果 Record 是一种要求所有可能的键实际存在于该类型的值中的类型,那么第一个示例不应导致错误。

如果 Record is 是一种要求所有可能的键实际存在于该类型的值中的类型,那么第二个示例也应该导致错误。在这种情况下,不可能构造该类型的值,因为可能的键集是无限的。

如果有第三种选择——这似乎是根据我尝试编译示例时实际发生的情况——它是什么?我可以看到这两种键类型之间的主要区别是一种具有有限的值集,另一种具有无限的值集。这是用来区分的吗?

除此之外,我能找到的唯一解释是 Record 不仅根据其键类型的值集进行区分,而且还根据其键类型的一些其他属性进行区分。如果是这样,键类型的什么属性会有所不同?还是 Record 做了相当于“绕过接口,转换为实现类型并做一些你不应该做的事情”的类型系统?

Record的实现是

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

我可以在这里发现两件事。第一个是将 K 绑定到“任意键”,但据我所知,这限制了可用于 K 的类型,而不是对结果类型有效的值。其次,我们有一个正常的索引签名,所以我的猜测是,我在 Record 中感到困惑的实际上是索引签名的行为——但由于其他问题,如果没有 Record,我无法轻松重现这种行为,所以我没有不想妄下结论。

【问题讨论】:

    标签: typescript types record


    【解决方案1】:

    让我们从实现开始:

    
    type Record<K extends keyof any, T> = {
        [P in K]: T;
    };
    

    keyof any - 仅表示可以用作任何对象的键的所有允许类型。 现在,为此您可以使用 PropertyKey 内置类型。

    {[P in K]: T;} 这只是一个常规的for..in 循环。

    因此,当您将联合类型 "foo"|"bar" 作为第一个参数传递给 Record 时,TS 编译器只会遍历每个联合并像这样创建:

    type Result = {
        foo:string,
        bar: string,
    }
    

    这意味着您的最终对象应具有最少的属性集:foobar

    但是,当您只是将 string 作为第一个参数传递时,情况就不同了。

    type Result = Record<string, string>
    
    type Result2 = {
        [P in string]: string
    }
    

    您可能已经注意到,ResultResult2 是相同的类型。

    现在,您可能认为Record&lt;string, string&gt; 等于索引接口:

    interface Indexed {
        [prop: string]: string
    }
    
    type Result = Record<string, string>
    
    type Check = Result extends Indexed ? true : false // true
    type Check2 =  Indexed extends Result  ? true : false // true
    
    

    但是这些类型的行为有点不同。 见this答案

    更新

    问题似乎仍然是 Record 是否要求所有可能的属性都实际存在。如果 Record 不要求所有可能的属性实际存在,为什么结果是具有必需字段而不是可选字段的对象类型?

    请看Mapped Types docs

    来自文档:

    映射类型建立在索引签名的语法之上,用于声明未提前声明的属性类型:

    因此,键入Record&lt;string, string&gt;,意味着您不确切知道将使用哪些键进行记录,但您100% 确定它将是string。这是设计使然。

    为什么Partial&lt;Record&lt;string, string&gt;&gt;Record&lt;string, string&gt;不一样,因为Partial表示值也可以是undefined

    换句话说,Partial&lt;Record&lt;string, string&gt;&gt; 等于 Record&lt;string,string | undefined&gt;

    它对于诸如字符串之类的“无限”类型是如何工作的

    表示如果key为string类型,可以使用任意字符串来完成这个要求。没有任何无穷大的字符串集。

    【讨论】:

    • 虽然 Record 和索引接口之间的区别本身就很有趣,但主要问题似乎在于联合类型与“字符串”的处理方式不同。问题似乎仍然是 Record 是否要求所有可能的属性实际存在。如果字符串不是这种情况,为什么联合类型会出现这种情况? IE。如果 Record 不要求所有可能的属性实际存在,为什么结果是具有必需字段而不是可选字段的对象类型?不是说它应该是,而是指出键类型“字符串”会发生等价的情况。
    • 我开始理解其中的区别——它似乎源于使用 [Property in K] 而不是 [Property: K]。后者不起作用,因为字符串和数字键的索引签名必须单独声明。由于“in K”(在 Record 中使用)似乎因为键枚举而有用,它对诸如字符串之类的“inifite”类型是如何工作的?这似乎与为什么对于字符串键删除所有键都存在的要求有关。
    • 我发现stackoverflow.com/questions/62881733/… 给出了另一个提示:映射类型不理会原始类型,因为这使它们最有用。从一开始我就想知道文字联合和原始类型字符串之间的区别是什么,所以我可以“理解它的一般情况”,但我现在意识到字符串-y 类型只能由文字构成、原始“字符串”以及使用运算符的组合。根本没有“一般情况”。我认为这回答了我的问题。
    猜你喜欢
    • 2023-03-03
    • 1970-01-01
    • 2011-05-15
    • 2016-10-30
    • 2013-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多