【问题标题】:String as variable and mapped key type behaves differently字符串作为变量和映射键类型的行为不同
【发布时间】:2021-12-04 07:53:05
【问题描述】:

考虑一下:

type N = never;
type A = 'A';
type B = 'A' | 'B';
type S = string;

type RN = Record<N, string>;
type RA = Record<A, string>;
type RB = Record<B, string>;
type RS = Record<S, string>;

declare let n : N;
declare let a : A;
declare let b : B;
declare let s : S;

s = b;
b = a;
a = n;

declare let rn : RN;
declare let ra : RA;
declare let rb : RB;
declare let rs : RS;

rn = rs;
rs = rn;

rs = ra;
ra = rb;

&lt; 成为子类型运算符。显然,N &lt; A &lt; B &lt; S 因为n 可分配给a 可分配给b 可分配给s

所以,我希望RS &lt; RB &lt; RA &lt; RN

但是,从示例中您可以看到 RB &lt; RA &lt; RS 因为rb 可分配给ra 可分配给rs。此外,RSRN 似乎是等价的类型。

我会假设 string 可以看作是所有 string 文字类型的联合类型。所以实际上RS 应该等于never 因为不可能有一个对象具有所有可能存在的字符串文字的属性(取无限 空间)。称其为完整对象。

但看起来 RS 实际上等同于空 (RN) 而不是完整的对象。

为什么stringRecord 中表现得像never

【问题讨论】:

    标签: typescript


    【解决方案1】:

    我假设字符串可以被视为所有字符串文字类型的联合类型。所以实际上RC 应该等于never,因为不可能有一个对象具有所有可能存在的字符串文字的属性(占用无限空间)。

    这是问题的症结所在。类型Record&lt;K, V&gt;,通常是任何具有索引签名的类型,应该表示其键是K 类型的值的对象,这样如果obj: Record&lt;K, V&gt;k: Kobj[k] 是类型V。如果K 是具有无限多个值的类型,那么由于您编写的原因,这在实践中是不可能的。因此,如果我们完全正式,则不可能构造 Record&lt;K, V&gt;,* 类型的值,因此如果 Typescript 完全正确,那么 Record&lt;string, V&gt; 和索引签名将无用。

    但 Typescript 并不完全正确,nor is it meant to be:

    TypeScript 的类型系统允许某些在编译时不知道的操作是安全的。当一个类型系统具有这个属性时,就说它不是“健全的”。仔细考虑了 TypeScript 允许不良行为的地方,并且在整个文档中,我们将解释这些情况发生的位置以及它们背后的激励场景。

    因此,索引签名和Record 以它们的方式工作,因为它对于编写假定对象以这种方式运行的 Javascript 代码的程序员很有用。真正的 Javascript 代码经常使用对象作为字典,并且经常使用已知存在的键而不处理键不存在的情况。

    对于Record&lt;string, V&gt; 的合理替代方案,您应该编写类似{[k in string]?: V} 的内容,以便类型系统明确知道并非所有可能的键都可能存在于对象中。在这种情况下,当您访问obj[k] 时,它将具有V | undefined 类型而不是V,并且您必须在代码中处理这种可能性(例如,通过使用if 语句缩小以检查该值是否为undefined)。


    *由于技术原因,当K 无限大时,这与Record&lt;K, V&gt; 不等于never 不同。这是semantically entailed,因为这两种类型具有相同的值集,但不是syntactically entailed,因为Typescript 将它们视为可相互分配的(因为它没有)。当K 为无限时,Typescript 没有规则将Record&lt;K, V&gt; 减少为never;我认为 Typescript 一开始就不会跟踪类型是否是无限的。

    【讨论】:

    • 对不起,我修改了我的问题,而你回答并没有添加过
    • 我认为这里的推理不太正确。 Record&lt;K, V&gt; 不一定有索引签名;它是一种映射类型,并且仅当键类型包括非文字(如string 或现在的`x${string}`)时才具有索引签名。索引签名意味着如果有一个索引类型的键,那么它的值有值类型。所以{[k: string]: number} 并不意味着“所有string 键都存在并且具有number 值”,而是“对于每个存在的string 键,它都有一个number 值”。几乎就像索引签名是 optional 属性(参见--noUncheckedIndexedAccess
    • 例如,Record&lt;"A" | "B", string&gt; 没有索引签名,因此需要 "A""B" 键。但是Record&lt;string, string&gt; 有一个string 索引签名,它根本不需要任何密钥。如果您不这么认为,我可能会发现 ahejlsberg 和其他 TS 团队成员的各种 github 问题 cmets 证实了这种索引签名的观点,即当键碰巧存在时对值的约束。
    • @jcalz 你能指点我的打字稿文档吗?
    • 今天学到了什么: 1. 如果键是非文字类型,则映射类型隐含索引签名,但文字类型键隐含必需的属性。 2. 索引签名在类型系统中是可选的,但默认情况下不要用 undefined 扩展值类型。但这可以通过 noUncheckedIndexAccess 强制执行。正确的@jcalz?
    【解决方案2】:

    Mapped types 类似于 the Record&lt;K, V&gt; utility type 映射 stringnumber literal 各个属性的键,因此 Record&lt;"A" | "B", string&gt; 等同于 {a: string; b: string}

    但是诸如string 本身或number 之类的宽非文字类型的键,或`foo${string}` 之类的模式模板文字类型(在microsoft/TypeScript#40598 中实现)被映射到index signatures。来自索引签名的文档:

    有时您无法提前知道类型属性的所有名称,但您确实知道值的形状。在这些情况下,您可以使用索引签名来描述可能值的类型。

    因此,索引签名并不真正代表具有相关类型的所有可能键的“完整对象”,例如所有单键对象{a: string} &amp; {b: string} &amp; {c: string} &amp; ... &amp; {foo: string} &amp; ... {blahblah: string} &amp; ... 的无限intersection

    (旁白:你说一个完整的对象将等同于never,因为这是不可能的。但这并不准确。一个Proxy 对象可以很容易地符合这种类型。即使它 在 JavaScript 中是不可能的,如果没有某种关于无穷大的明确公理,你会希望类型系统将其视为 never,这并不明显,然后你会必须弄清楚如何在不禁止递归数据类型的情况下做到这一点。)

    无论如何,索引签名更像是属性上的约束{[k: IndexType]: ValType} 形式的索引签名意味着“如果该对象具有IndexType 类型的属性键,那么这样的属性将具有@987654350 类型的值@"。从某种意义上来说,更像是所有单键对象与optional properties的无限交集,比如{a?: string} &amp; {b?: string} &amp; {c?: string} &amp; ... &amp; {foo?: string} &amp; ... {blahblah?: string} &amp; ...


    当然比这更复杂,因为编译器在传统上并未将索引签名和可选属性视为相同。

    在 TypeScript 4.1 之前,索引签名总是可以让你读取属性并获得一个值,即使我刚刚解释完它们更像是可选属性。对此有很多抱怨,因此 TypeScript 4.1 引入了the --noUncheckedIndexedAccess compiler flag,它在读取时将undefined 添加到索引签名属性值的域中,但在写入时不添加。即使使用--strict,默认情况下也不会启用它,因为虽然它的类型更安全,但在人们通过数组或对象索引的任何情况下都会很烦人......像for (let i=0; i&lt;arr.length; i++) {arr[i]}Object.keys(obj).forEach(k =&gt; obj[k])这样的代码应该在技术上显示arr[i]obj[k] 可能是undefined,至少没有办法跟踪ik身份 而不仅仅是类型.

    在 TypeScript 4.4 之前,可选属性在阅读写作时都被视为将undefined 作为其域的一部分。人们对此也有很多抱怨,因此 TypeScript 4.4 引入了the --exactOptionalPropertyTypes compiler flag,它在读取时保留了undefined,但拒绝写入带有undefined 的属性。 --strict 也没有包含此内容,因为如果 bar 是可选的,则类似 foo.bar = foo.bar 的内容现在被视为错误。

    如果您启用这两个编译器标志,则索引签名和可选属性具有相似的行为,尽管我确信存在更多边缘情况。


    无论如何...Record&lt;string, string&gt; 等同于{[k: string]: string}) 而Record&lt;never, string&gt; 等同于empty object type {}。它们不是相同的类型,但由于与microsoft/TypeScript#7029 中实现的隐式索引签名相关的规则,它们是相互兼容的。

    那里也有很多东西要解开,关于weak type detectionexcess property checking,以及索引签名和interface 类型之间的交互(参见microsoft/TypeScript#15300)。不过,我现在要停下来了,因为这个答案已经够长了。

    【讨论】:

    • 非常感谢您的详细解答!你对完整的对象是正确的。当然它可以存在,因为覆盖索引 getter 并返回一些东西模仿了一个完整的对象。
    猜你喜欢
    • 1970-01-01
    • 2012-07-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-13
    • 2020-01-17
    • 2017-11-20
    • 2012-01-23
    相关资源
    最近更新 更多