← 返回导航

可选属性 ?: vs | undefined

踩坑记录

场景说明

在定义接口时,{ 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

// 场景一:表单字段 -- 用 ?:
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;
}
在 Playground 中尝试