TypeScript中带字符串键的对象为何匹配数字索引签名类型?
这个问题真的很容易让人摸不着头脑,我刚学TypeScript的时候也被这个规则坑过!咱们一步步拆解为什么会出现你看到的现象。
首先先确认你的代码和现象:
type Arrayish = { [n: number]: boolean }; type Check<T> = T extends Arrayish ? "Yes" : "No"; type Result = Check<{ abc: true }>; // 你预期是"No",但实际结果是"Yes"
核心原因:TypeScript的数字索引签名子类型检查规则
TypeScript对数字索引签名的子类型判断,不是看键的类型是否为数字,而是看目标对象的所有属性值类型是否匹配索引签名要求的类型,同时允许额外的字符串键存在——只要这些键对应的属性值类型符合索引签名的要求。
在你的例子里,{ abc: true }的唯一属性值是true,属于boolean类型,完全匹配Arrayish中数字索引签名指定的boolean类型。所以TypeScript会判定这个对象是Arrayish的子类型,因此Check<{ abc: true }>返回"Yes"。
为什么TypeScript要这么设计?
这其实是为了兼容JavaScript的底层行为:在JS运行时,所有对象的键最终都会被转换为字符串(比如你写obj[0],实际访问的是obj["0"])。TypeScript的类型系统并没有完全割裂和JS的联系,而是做了妥协——它没有严格限制键的字面量类型,而是聚焦在属性值的类型一致性上:只要所有属性的类型都符合索引签名的要求,不管键是字符串、数字还是其他形式,都被认为是兼容的。
如何让{ abc: true }返回"No"?
如果你希望只有包含数字键(或可转换为数字的字符串键)的对象才匹配Arrayish,可以给类型加上字符串索引签名并设为never,以此禁止非数字转换的字符串键存在:
type Arrayish = { [n: number]: boolean; [s: string]: never; // 禁止存在不符合数字键规则的字符串键 }; type Check<T> = T extends Arrayish ? "Yes" : "No"; type Result = Check<{ abc: true }>; // 现在结果是"No" type Result2 = Check<{ 0: true }>; // 结果还是"Yes",符合预期
这里的逻辑是:TypeScript规定,如果同时定义了数字和字符串索引签名,数字索引的类型必须能赋值给字符串索引的类型。把字符串索引设为never后,普通字符串键对应的属性类型(比如boolean)无法赋值给never,自然就会被排除在外。
内容来源于stack exchange

