You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何与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的实现逻辑共同导致的:

  1. 先看TS内置Omit的默认实现,逻辑非常直接:
    type Omit<T, K extends keyof any> = {
        [P in Exclude<keyof T, K>]: T[P]
    }
    
    本质是用映射类型,遍历原类型T里排除掉K之后的所有键,重新构造一个新的对象类型。
  2. 第一步计算keyof B:B是A和Record<string, any>的交叉类型,Record<string, any>自带全字符串索引签名,意味着任意字符串键都是这个类型的合法键,所以keyof B的结果是宽泛的string类型,不是我们直觉里的字面量联合'a' | 'b'。
  3. 第二步计算Exclude<keyof B, 'a'>:从宽泛的string类型里去掉单个字面量'a',结果依然是string,根本不会收敛到'b'这种具体的字面量联合。
  4. 最后一步构造映射类型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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 10:03:22