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

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静态类型检查的规则差异导致的,和运行时值无关:

  1. 显式类型标注的优先级最高
    当你写const pluginOptions: Options = xxx时,相当于主动告诉TypeScript:pluginOptions的静态类型就是Options,即{ foo: boolean } | { foo: boolean; bar: boolean }这个联合类型。
    对于联合类型,TypeScript默认只允许访问所有联合成员共有的属性:foo是两个成员都有的,所以可以直接访问;bar只在第二个成员中存在,从类型层面看pluginOptions有可能是第一个没有bar的结构,因此直接访问会报类型错误。
    JSON.stringify判断两个对象值相等是运行时逻辑,TypeScript的类型检查是编译时的静态分析,不会参考运行时的相等结果。
  2. 对象字面量的特殊窄化逻辑
    当赋值语句右侧是直接书写的新鲜对象字面量时,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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 21:51:31