先记住这个答案
对对象联合执行某个键的 in 检查,TypeScript 会据此筛选可能拥有该属性的成员。必需属性倾向于进入真分支,缺少属性的成员进入假分支,而可选属性对应的成员可能同时保留在两边,因为它既可能提供也可能省略该键。in 的运行时检查包含原型链,且属性存在不等于属性值有定义或可调用。处理 unknown 时还要先确认非空对象,再检查属性和值类型,不能直接对任意输入使用 in。
- in 按属性存在性建立联合分支证据
- 可选属性的成员可能同时保留在真假两边
- 键存在、值有效与自有属性是不同条件
可选成员为何无法被完全排除
假设联合中有文本结果、可重试结果,以及允许可选重试回调的扩展结果。扩展结果可能写回调,也可能不写,所以检查 retry 后,不能仅凭真假就把它归入某一个固定方向。理解这种可能性有助于解释分支内仍然出现联合的提示。
示例在真分支继续检查回调是否是函数,随后才调用。扩展类型的可选属性允许显式 undefined,刻意展示键存在却不能调用的情况。外部未知对象也应分层检查,不能因为找到键就直接断言它是完整业务接口。
type Fixed = { kind: 'fixed'; text: string };
type Retriable = { kind: 'retry'; retry: () => string };
type Extension = { kind: 'extension'; retry?: (() => string) | undefined };
function attempt(value: Fixed | Retriable | Extension): string {
if ('retry' in value) {
if (typeof value.retry === 'function') return value.retry();
return 'present but unavailable';
}
return value.kind;
}
const withUndefined: Extension = { kind: 'extension', retry: undefined };
const withoutKey: Extension = { kind: 'extension' };可选成员既能包含 retry: undefined,也能完全省略键,所以两次调用可能走不同分支。即使开启精确可选属性检查,这里显式联合 undefined 的声明仍允许存在但不可调用的值。
普通读取为什么不能替代存在判断
直接访问联合中的特有字段可能在访问动作本身就报错,因为其他成员没有该属性。in 可以先建立存在性的证据,再读取并验证值。它还区分缺少键与键存在但值为 undefined,而普通比较某次读取结果无法总是表达这种差异。
运行时 in 会搜索原型链,因此继承来的属性也能让结果为真。若解析协议要求字段必须由对象自身提供,应另行采用自有属性检查,并确认所用 API 的类型定义是否提供相同收窄能力。运行时语义和编译器识别的守卫形式需要分别核对。
属性变化后应重新审查假设
如果对象可以被其他代码增加、删除或重写属性,较早的存在检查未必代表稍后的真实状态。可以在检查后读取需要的稳定值,或限制对象的修改范围。对于可能带访问器或 Proxy 的输入,属性读取还可能执行额外代码,不应把普通数据对象假设无限推广。
设计新的业务联合时,若分支必须被明确识别,优先使用互斥的稳定标签,再把回调等能力放在对应成员里。in 适合按现有能力区分结构,但很多可选字段混杂时,它会保留较多候选,也更容易让代码反复检查同一能力。
容易答错的地方
- in 为真说明属性值不是 undefined
- 属性可以明确存在但保存 undefined,尤其是允许该值的可选字段;使用前仍需依据目标操作验证值类型和必要范围。
- in 检查的是对象自己的字段
- 它也会沿原型链查找,不能当作自有属性证明;解析外部协议时应明确是否允许继承字段,并使用符合约定的检查方式。
面试官还会怎么问?
为什么 in 的假分支还会有可选属性成员?
可选属性可以被省略,那个成员仍然可能满足假分支,因此编译器不能把它完全排除;需要唯一分支时应增加明确判别标签。
对 unknown 直接执行 in 安全吗?
不安全,输入可能是空值或原始值;应先确认非空对象,再检查键,随后继续验证字段值,而不是一次判断后断言全部结构。
exactOptionalPropertyTypes 会解决所有问题吗?
它改变省略属性与显式 undefined 的可赋值规则,但属性类型仍可显式允许 undefined,也不会把 in 改成自有属性或完整值验证。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。