为何与Record<string, any>交叉的类型经Omit后属性类型变为any?
问题现象
在如下TypeScript代码示例中,c.b的类型被意外推断为any:
interface A { a: string; b: string; } type B = A & Record<string, any> declare let b: B; b.b; // 类型正常推断为string type C = Omit<B, 'a'>; declare let c: C; c.b; // 类型被推断为any
实际表现:
- 类型为
B的变量b,访问b属性时可正常得到string类型 - 使用内置
Omit工具类型从B中剔除'a'属性得到类型C后,同名字段c.b的类型被推断为any
产生原因
这个现象是TypeScript的类型计算规则和内置Omit的实现逻辑共同导致的:
- 先看TS内置
Omit的默认实现,逻辑非常直接:
本质是用映射类型,遍历原类型type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P] }T里排除掉K之后的所有键,重新构造一个新的对象类型。 - 第一步计算
keyof B:B是A和Record<string, any>的交叉类型,Record<string, any>自带全字符串索引签名,意味着任意字符串键都是这个类型的合法键,所以keyof B的结果是宽泛的string类型,不是我们直觉里的字面量联合'a' | 'b'。 - 第二步计算
Exclude<keyof B, 'a'>:从宽泛的string类型里去掉单个字面量'a',结果依然是string,根本不会收敛到'b'这种具体的字面量联合。 - 最后一步构造映射类型
C的时候,遍历的键是宽泛的string,这时候取B[P]会直接命中Record<string, any>的索引签名,返回any类型,根本不会保留接口A里明确定义的b: string这个具体类型。
至于直接访问b.b能拿到string类型,是TS给交叉类型的直接属性访问做的特殊优化:你用固定的字面量属性名(比如'b')访问交叉类型时,TS会优先找接口里明确定义的具体属性,匹配到了就直接返回对应类型,只有找不到具体属性的时候才会走索引签名的兜底类型。但这个优化只在你直接写属性名访问的时候生效,构造映射类型、遍历宽泛键的时候不会触发,所以Omit执行完就把具体属性的类型信息丢了。
修复方案
要保留已知属性的类型推断很简单,别先交叉索引签名再做Omit,先对已知属性做Omit,再交叉索引签名就行:
type FixedC = Omit<A, 'a'> & Record<string, any> declare let cFixed: FixedC; cFixed.b; // 类型正常推断为string
内容的提问来源于stack exchange,提问作者Alexander Pankin
相关产品推荐
相关产品推荐

