typescript - 字符串作为变量和映射键类型的行为不同
问题描述
考虑一下:
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
因为n
is assignable toa
是 assignable tob
是 assignable to s
。
所以,我会期待RS < RB < RA < RN
.
但是,从示例中您可以看到,RB < RA < RS
因为rb
is assignable tora
是 assignable to rs
。而且,RS
似乎RN
是等价的类型。
我假设string
可以将其视为所有string
文字类型的联合类型。所以实际上RS
应该等于,never
因为不可能有一个对象具有所有可能存在的字符串文字的属性(占用无限空间)。将此称为完整对象。
然而,它看起来RS
实际上等同于空 ( RN
) 而不是完整的对象。
为什么string
表现得像never
in Record
?
解决方案
映射类型,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
,至少没有一种跟踪身份的方法i
和k
而不仅仅是type。
在 TypeScript 4.4 之前,可选属性在读写时都被视为undefined
其域的一部分。人们也经常抱怨这一点,因此 TypeScript 4.4 引入了编译器标志,该标志在读取时保留,但拒绝写入带有. 这也不包含在 中,因为如果是可选的,则现在将类似的内容视为错误。--exactOptionalPropertyTypes
undefined
undefined
--strict
foo.bar = foo.bar
bar
如果您启用这两个编译器标志,则索引签名和可选属性具有相似的行为,尽管我确信存在更多边缘情况。
不管怎样...Record<string, string>
等价于{[k: string]: string}
) whileRecord<never, string>
等价于空对象类型 {}
。这些不是相同的类型,但由于与microsoft/TypeScript#7029中实现的隐式索引签名有关的规则,它们是相互兼容的。
那里也有很多东西要解压,关于弱类型检测、过多属性检查以及索引签名和interface
类型之间的交互可能会持续很长时间(参见microsoft/TypeScript#15300)。不过,我现在要停下来了,因为这个答案已经足够长了。