exactOptionalPropertyTypes 会改变可选属性的什么语义?
简化版
exactOptionalPropertyTypes 会让可选属性 foo?: T 更精确地表示“属性可以不存在”,而不是默认等同于“属性存在且值可以是 T | undefined”。
开启后,如果你想显式赋值 undefined,需要把属性类型写成 foo?: T | undefined。它能让配置合并、JSON 序列化、PATCH 请求等场景更贴近运行时语义。
详细版
不开启时:
type Options = {
theme?: "dark" | "light";
};
const options: Options = {
theme: undefined
};
开启后,上面会报错。因为 theme?: T 表示可以没有 theme,不表示可以显式写 theme: undefined。
如果确实允许:
type Options = {
theme?: "dark" | "light" | undefined;
};
这能区分“没传”和“传了 undefined”。
完整版教学
一、可选属性有两层含义
可选属性首先表示属性可以缺失。
type User = {
nickname?: string;
};
运行时有两种不同对象:
const a = {};
const b = { nickname: undefined };
这个选项关注的是“属性不存在”和“属性存在但值为 undefined”的区别。
二、默认语义比较宽松
默认情况下,TypeScript 常把可选属性当成读取时可能是 T | undefined,写入时也允许显式写 undefined。
const user: User = {
nickname: undefined
};
这对宽松 JS 兼容友好,但不够精确。
三、开启后的写入更精确
开启 exactOptionalPropertyTypes 后:
type Config = {
cache?: boolean;
};
const c1: Config = {};
const c2: Config = { cache: true };
// const c3: Config = { cache: undefined };
如果业务允许显式 undefined,要写出来:
type Config = {
cache?: boolean | undefined;
};
四、读取时仍要处理 undefined
即使开启后,读取可选属性时仍然可能得到 undefined。
function use(config: Config) {
const cache = config.cache;
}
因为属性可能不存在,读取结果自然可能是 undefined。
| 操作 | 影响 |
|---|---|
写入 { key: undefined } | 更严格 |
读取 obj.key | 仍可能是 undefined |
判断 "key" in obj | 可区分是否存在 |
五、为什么对配置合并有价值
配置对象里,缺失和显式 undefined 可能语义不同。
const defaults = { retry: 3 };
const user = { retry: undefined };
有些合并逻辑会把 undefined 当成覆盖值,有些会忽略。精确可选属性能迫使类型设计说清楚。
六、和接口请求的关系
PATCH 请求常用“字段缺失表示不修改”,而 field: undefined 可能无法序列化,或者被后端解释成清空。
type PatchUser = {
name?: string;
};
这个类型更适合表示“可以不传 name”。如果允许清空,可能需要 name?: string | null 明确建模。
七、常见误区与追问
- 误区:可选属性就等于
T | undefined。 开启精确选项后,写入语义不再等价。 - 误区:开启后读取可选属性不需要判空。 属性可能不存在,读取仍可能是 undefined。
- 误区:显式 undefined 和缺失属性运行时完全一样。
in、Object.keys、序列化和合并逻辑可能不同。 - 误区:所有可清空字段都应该用 undefined。 接口语义常用
null明确表达清空。 - 追问:如果确实允许 undefined 怎么写? 写成
foo?: T | undefined。 - 追问:这个选项适合什么项目? 对配置、接口补丁、序列化语义要求清晰的项目。
八、加强记忆
记忆对比:
foo?: T -> may be absent
foo?: T | undefined -> may be absent or explicitly undefined
回答时抓住“缺失”和“显式 undefined”的区别,再落到配置合并和 PATCH 请求,会比只背 tsconfig 名称更有说服力。