Skip to content

第 21 条:一次性创建对象

要点

  • 更倾向于一次性构建对象,而不是逐步添加属性。
  • 使用多个对象和对象展开语法({...a, ...b})以类型安全的方式添加属性。
  • 知道如何有条件地向对象添加属性。

正文

正如第 19 条所说,虽然变量的值可以改变,但在 TypeScript 中变量的类型通常不会变。这让某些 JavaScript 写法在 TypeScript 中不好实现,尤其是建议你一次性创建完整对象,而不是一点一点地给对象添加属性。

JavaScript 中,你可能会这样创建一个二维点对象:

js
const pt = {}
pt.x = 3
pt.y = 4

但是在 TypeScript 中,这样写会报错:

ts
const pt = {}
pt.x = 3 // 报错:{} 类型上不存在属性 'x'
pt.y = 4 // 报错:{} 类型上不存在属性 'y'

💻 playground

因为 pt 的类型是根据初始化值 {} 推断出来的,只能赋值给已知的属性。

如果你定义了一个接口 Point,再用空对象赋值,也会有问题:

ts
interface Point {
  x: number
  y: number
}
const pt: Point = {} // 报错:{} 缺少 'x' 和 'y' 属性
pt.x = 3
pt.y = 4

💻 playground

用类型断言似乎解决了问题:

ts
const pt = {} as Point
//    ^? const pt: Point
pt.x = 3
pt.y = 4 // OK

💻 playground

但这样做有风险:TypeScript 不会检查你是否给 pt 赋齐所有属性,少赋了某个属性,类型检查也会通过,可能导致运行时出错。类型断言不应当成为首选方案。

最佳做法是一次性定义完整对象:

ts
const pt: Point = { x: 3, y: 4 }

如果要用小对象组合成大对象,避免多步赋值:

ts
const pt = { x: 3, y: 4 }
const id = { name: 'Pythagoras' }
const namedPoint = {}
Object.assign(namedPoint, pt, id)
namedPoint.name // 报错:{} 类型没有 'name' 属性

💻 playground

应该用对象展开语法一次性合并:

ts
const namedPoint = { ...pt, ...id }
// 类型是 { name: string; x: number; y: number; }
namedPoint.name // 不报错

💻 playground

你也可以用对象展开语法分步构造对象,每次都用新变量,这样每个变量都会有新的类型(符合第 19 条):

ts
const pt0 = {}
const pt1 = { ...pt0, x: 3 }
const pt: Point = { ...pt1, y: 4 } // OK

💻 playground

最后一行的类型声明确保属性都已赋齐。虽然写法有点绕,但对复杂对象分步构建很有用。

要有条件地添加属性,可以用展开和空对象或假值(null、undefined、false 等)来避免加属性:

ts
declare let hasMiddle: boolean
const firstLast = { first: 'Harry', last: 'Truman' }
const president = { ...firstLast, ...(hasMiddle ? { middle: 'S' } : {}) }
//    ^? const president: {
//         middle?: string;
//         first: string;
//         last: string;
//       }
// or: const president = {...firstLast, ...(hasMiddle && {middle: 'S'})};

💻 playground

这样推断出的类型中 middle 是可选属性。

你也可以用类似方法同时条件添加多个字段:

ts
declare let hasDates: boolean
const nameTitle = { name: 'Khufu', title: 'Pharaoh' }
const pharaoh = { ...nameTitle, ...(hasDates && { start: -2589, end: -2566 }) }
//    ^? const pharaoh: {
//         start?: number;
//         end?: number;
//         name: string;
//         title: string;
//       }

💻 playground

如果你从 pharaoh 取出 start,类型是 number | undefined,需要考虑它可能是 undefined

ts
const { start } = pharaoh // 类型 number | undefined

有时你需要通过转换另一个对象来创建新对象或数组,这时“全量创建”的思路对应于用内置的函数式方法或像 Lodash 这样的工具库,而不是用循环。第 26 条有更多相关内容。

关键点总结

  • 更倾向于一次性构建对象,而不是逐步添加属性。
  • 使用多个对象和对象展开语法({...a, ...b})以类型安全的方式添加属性。
  • 知道如何有条件地向对象添加属性。

Released under the MIT License.