为什么TypeScript中string作为Record的键时表现和never类似?
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;
这个现象的核心原因是TypeScript对有限字面量联合类型和无限原始类型作为映射类型键时的不同处理逻辑,以及结构类型兼容性的规则:
- 首先明确
Record的内置定义:
它本质是一个映射类型,会遍历泛型type Record<K extends keyof any, T> = { [P in K]: T }K的所有成员,生成对应属性为T类型的对象类型。你提到的键型逆变预期并不适用于普通映射类型,逆变是函数参数类型的特有规则,映射类型的可赋值性遵循结构兼容原则,而非参数位置的逆变规则。 - 当
K是有限的字符串字面量联合(比如'A'、'A'|'B')时,TypeScript会枚举所有联合成员,生成对应必选属性的对象类型:RA = Record<'A', string>等价于{ A: string }RB = Record<'A'|'B', string>等价于{ A: string; B: string }
按照结构兼容性规则,拥有更多属性的RB完全满足RA的结构要求,所以RB < RA的子类型关系成立,和实际赋值行为一致。
- 当
K是string这类无限的原始类型时,TypeScript不会真的枚举所有可能的字符串字面量生成属性(逻辑上不可能实现),而是会把{ [P in string]: T }直接等价于字符串索引签名{ [key: string]: T }。索引签名的语义是:该对象所有字符串类型的键对应的属性值,都必须符合T类型,而非要求对象包含所有可能的字符串键,这和你认知里的「完全对象」定义不同,是TypeScript的刻意设计,避免出现不可能实现的类型。 - 至于
RS和RN互相赋值的表现,原因如下:RN = Record<never, string>的含义是没有任何属性的对象(never是没有任何成员的联合类型,映射后不会生成任何属性),等价于空对象类型{}。- 按照TypeScript的结构兼容性规则:空对象类型
{}兼容所有非null/undefined的对象类型,所以带索引签名的RS类型可以赋值给RN;反过来空对象没有任何字符串属性,自然满足RS的索引签名要求(没有违反值类型约束的属性),所以RN也可以赋值给RS,二者在结构上兼容,就出现了看起来等价的表现。
内容的提问来源于stack exchange,提问作者user3612643
相关产品推荐
相关产品推荐

