在定义接口时,{ name?: string } 和 { name: string | undefined } 看起来很像,但语义完全不同。可选属性表示"属性可以不存在",而 | undefined 表示"属性必须存在,但值可以是 undefined"。这个细微差别在序列化、展开操作和 exactOptionalPropertyTypes 编译选项下会产生截然不同的行为。
// 可选属性 ?: vs | undefined 的核心区别
interface WithOptional {
name?: string; // 属性可以不存在
age: number;
}
interface WithUndefined {
name: string | undefined; // 属性必须存在,值可以是 undefined
age: number;
}
// 区别一:属性是否存在
const a: WithOptional = { age: 18 }; // OK,name 不存在
const b: WithOptional = { age: 18, name: undefined }; // OK,name 显式 undefined
const c: WithUndefined = { age: 18, name: undefined }; // OK
// const d: WithUndefined = { age: 18 }; // 错误!缺少 name 属性
// 区别二:JSON.stringify 行为不同
const opt: WithOptional = { age: 18 };
console.log(JSON.stringify(opt)); // {"age":18} -- name 不存在,不输出
const und: WithUndefined = { age: 18, name: undefined };
console.log(JSON.stringify(und)); // {"age":18,"name":...} -- name 存在但值为 undefined,JSON 会忽略 undefined 值
// 区别三:exactOptionalPropertyTypes 编译选项
// tsconfig 中开启 exactOptionalPropertyTypes: true 后:
// { age: 18, name: undefined } 不能赋值给 WithOptional
// 因为 name 是可选属性,undefined 只在属性不存在时才是合法的
// 区别四:in 操作符检查
function checkName(obj: WithOptional) {
if ('name' in obj) {
console.log(obj.name); // string | undefined,属性存在但值可能是 undefined
}
}
// 实际踩坑:接口返回 null 但类型写了 ?:
interface User {
id: number;
nickname?: string; // 后端可能返回 null 而不是不返回该字段
}
// 后端返回 { id: 1, nickname: null }
// 如果用了 ?: ,访问 user.nickname.length 不会报 TS 错误
// 但运行时 user.nickname 是 null,.length 会报错
// 正确做法:明确类型
interface UserSafe {
id: number;
nickname: string | null; // 明确表示可能为 null
}
?: 表示属性可以不存在(可选的),| undefined 表示属性必须存在但值可以是 undefined。这是两种不同的意图。?: 允许对象不包含该属性,| undefined 要求对象必须包含该属性(即便是 undefined)。| undefined 的属性始终存在(但 undefined 值在 JSON.stringify 时会被忽略或转为 null)。?: 属性不允许显式赋值为 undefined,只能通过不传该属性来体现"可选"。?: 定义类型,TS 不会提示你检查 null,容易导致运行时错误。// 选择指南:什么时候用 ?: ,什么时候用 | undefined
// 场景一:表单字段 -- 用 ?:
interface FormData {
name: string;
email?: string; // 选填字段,可以不传
phone?: string; // 选填字段
}
// 场景二:API 响应 -- 用 | null
interface ApiUser {
id: number;
avatar: string | null; // 后端明确返回 null 表示无头像
nickname: string | null;
}
// 场景三:配置对象 -- 用 ?:
interface Options {
url: string;
timeout?: number; // 可选配置,不传则用默认值
retry?: number; // 可选配置
}
// 场景四:明确的"无值"状态 -- 用 | undefined
interface State {
data: string | undefined; // 初始状态为 undefined,加载后为 string
error: string | undefined;
}