TypeScript中对象联合类型未按预期工作的问题咨询
问题描述
假设定义如下Config接口,以及符合该接口约束的变量:
interface Config { plugins: { name1: { foo: boolean; }, name2: { foo: boolean; bar: boolean; } } } const config: Config = { plugins: { name1: { foo: true, }, name2: { foo: true, bar: false } } }
定义Options类型,用于表示plugins属性下所有值类型组成的联合类型:
type Options = Config["plugins"][keyof Config["plugins"]];
TypeScript将该类型推导为{ foo: boolean } | { foo: boolean; bar: boolean },但实际使用时出现类型错误:
const pluginOptions: Options = config.plugins.name2; const foo = pluginOptions.foo; const bar = pluginOptions.bar; // 此处报错 // Property 'bar' does not exist on type 'Options'. // Property 'bar' does not exist on type '{ foo: boolean; }'
但直接传入对象字面量却可以正常工作:
const equal = JSON.stringify(config.plugins.name2) === JSON.stringify({ foo: true, bar: false }); console.log(equal); // true,两个对象完全一致! const pluginOptions2: Options = { foo: true, bar: false }; const foo2 = pluginOptions2.foo; const bar2 = pluginOptions2.bar;
如果给插件name1的类型定义也加上bar属性,上述报错就会消失。需求是校验传入变量的类型合法性,要求值只能是{ foo: boolean }或{ foo: boolean; bar: boolean }两种结构,疑问点为:
- 为什么取值自
config.plugins.name2时访问bar属性报错,直接使用结构完全相同的对象字面量却正常? - 现有写法是否存在问题,如何正确实现需求?
该问题可在TypeScript Playground复现。
问题原因
这个现象是TypeScript静态类型检查的规则差异导致的,和运行时值无关:
- 显式类型标注的优先级最高
当你写const pluginOptions: Options = xxx时,相当于主动告诉TypeScript:pluginOptions的静态类型就是Options,即{ foo: boolean } | { foo: boolean; bar: boolean }这个联合类型。
对于联合类型,TypeScript默认只允许访问所有联合成员共有的属性:foo是两个成员都有的,所以可以直接访问;bar只在第二个成员中存在,从类型层面看pluginOptions有可能是第一个没有bar的结构,因此直接访问会报类型错误。JSON.stringify判断两个对象值相等是运行时逻辑,TypeScript的类型检查是编译时的静态分析,不会参考运行时的相等结果。 - 对象字面量的特殊窄化逻辑
当赋值语句右侧是直接书写的新鲜对象字面量时,TypeScript会执行特殊处理:
- 先做超额属性检查,禁止出现联合类型所有成员都不存在的属性;
- 再根据字面量的实际结构,把左侧变量的类型窄化到匹配的具体联合成员,而不是保留宽泛的整个联合类型。
所以写const pluginOptions2: Options = { foo: true, bar: false }时,TS识别到这个字面量匹配联合类型的第二个成员,会把pluginOptions2的类型窄化为{ foo: boolean; bar: boolean },访问bar自然不会报错。
但如果右侧是从已有变量/属性上取的值(比如config.plugins.name2),这个值已经丢失了「新鲜字面量」的标记,TS只会做最基础的类型兼容性检查(判断右侧值的类型是否可以赋值给Options),不会修改你显式标注的左侧变量类型,因此pluginOptions始终是整个联合类型,访问bar就会报错。
给name1的类型加上bar属性后报错消失,是因为此时两个联合成员都有bar属性,bar变成了联合类型的公共属性,自然可以直接访问。
实现需求的正确方式
Options类型定义本身是正确的,要实现「校验值符合两种结构之一,同时不丢失精确类型」的需求,有两种常用方案:
方案1:用satisfies运算符做校验(推荐,TS 4.9+支持)
satisfies会校验值是否符合目标类型,但不会把值的类型拓宽为目标类型,会保留值本身的精确类型:
// 校验config.plugins.name2符合Options类型,同时保留它原有的带bar属性的类型 const pluginOptions = config.plugins.name2 satisfies Options; const foo = pluginOptions.foo; // 正常 const bar = pluginOptions.bar; // 正常,类型被保留为boolean
如果场景是定义配置项,直接给整个config用satisfies效果更好,从根源避免类型被拓宽:
const config = { plugins: { name1: { foo: true }, name2: { foo: true, bar: false } } } satisfies Config; // 此时config.plugins.name2的类型会保留精确结构,不需要额外标注 const bar = config.plugins.name2.bar; // 正常访问
方案2:访问非公共属性前做类型收窄
如果确实需要把变量标注为Options联合类型,在访问bar之前通过类型守卫收窄类型即可:
const pluginOptions: Options = config.plugins.name2; const foo = pluginOptions.foo; // 用in运算符做类型守卫 if ('bar' in pluginOptions) { const bar = pluginOptions.bar; // 块级作用域内类型被收窄,访问正常 }
内容的提问来源于stack exchange,提问作者MZPL
相关产品推荐
相关产品推荐

