首页 > 解决方案 > 字符串作为变量和映射键类型的行为不同

问题描述

考虑一下:

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;

<成为子类型运算符。显然,N < A < B < S因为nis assignable toa是 assignable tob是 assignable to s

所以,我会期待RS < RB < RA < RN.

但是,从示例中您可以看到,RB < RA < RS因为rbis assignable tora是 assignable to rs。而且,RS似乎RN是等价的类型。

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

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

为什么string表现得像neverin Record

标签: typescript

解决方案


映射类型Record<K, V>实用程序类型映射string和单个属性的number 文字键,因此Record<"A" | "B", string>等效于{a: string; b: string}.

string但是像它本身这样的宽的非文字类型的键,或者像(在microsoft/TypeScript#40598number中实现的)模式模板文字类型的键被映射到索引签名。从索引签名的文档中:`foo${string}`

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

所以索引签名并不真正代表具有相关类型的所有可能键的“完整对象”,就像所有单键对象的无限交集{a: string} & {b: string} & {c: string} & ... & {foo: string} & ... {blahblah: string} & ...

(顺便说一句:你说一个完整的对象将等价于never因为它是不可能的。但这并不准确。一个Proxy对象可以很容易地符合这种类型。即使在 JavaScript不可能,也不会很明显你想要一个类型系统把它当作它来对待never,而不需要某种关于无穷大的明确公理,然后你必须弄清楚如何在不禁止递归数据类型的情况下做到这一点。)

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


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

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

在 TypeScript 4.4 之前,可选属性在读写时都被视为undefined其域的一部分。人们也经常抱怨这一点,因此 TypeScript 4.4 引入编译器标志,该标志在读取时保留,但拒绝写入带有. 这也不包含在 中,因为如果是可选的,则现在将类似的内容视为错误。--exactOptionalPropertyTypesundefinedundefined--strictfoo.bar = foo.barbar

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


不管怎样...Record<string, string>等价于{[k: string]: string}) whileRecord<never, string>等价于空对象类型 {}。这些不是相同的类型,但由于与microsoft/TypeScript#7029中实现的隐式索引签名有关的规则,它们是相互兼容的。

那里也有很多东西要解压,关于弱类型检测过多属性检查以及索引签名和interface类型之间的交互可能会持续很长时间(参见microsoft/TypeScript#15300)。不过,我现在要停下来了,因为这个答案已经足够长了。


推荐阅读