
对象是 JavaScript 的核心，我们用它来组织和传递数据，构建应用的几乎所有部分。然而，在原生 JavaScript 中，对象的结构是动态且松散的。我们可以在任何时候添加、删除或修改其属性，这种灵活性在带来便利的同时，也埋下了无数潜在的运行时错误。

TypeScript 通过其强大的类型系统，为对象这匹“野马”套上了缰绳。它允许我们精确地定义对象的“形状”（Shape），确保每个对象都拥有我们所期望的属性和类型。本文将深入探讨如何定义对象类型，并介绍两个极其重要的修饰符：可选属性和只读属性。

### 使用类型注解定义对象结构

在 TypeScript 中，为对象定义结构的最直接方式是使用类型注解。我们通过一种类似于对象字面量的语法，来描述对象应该包含哪些属性，以及每个属性的类型。

假设我们需要定义一个表示用户信息的对象：

```typescript
let user: {
  id: number;
  username: string;
  email: string;
};

// 正确的赋值
user = {
  id: 1,
  username: "alice",
  email: "alice@example.com",
};

// 错误的赋值 (会引发编译时错误)
// user = { id: 2, username: "bob" }; // 错误: Property 'email' is missing.
// user = { id: 3, username: "charlie", email: "charlie@example.com", isAdmin: true }; // 错误: Object literal may only specify known properties.
```

通过 `{ id: number; username: string; email: string; }` 这段注解，我们与 TypeScript 编译器达成了一个契约：
1.  任何赋值给 `user` 变量的对象，**必须**拥有 `id`、`username` 和 `email` 这三个属性。
2.  这些属性的类型**必须**分别是 `number`、`string` 和 `string`。
3.  **不允许**缺少任何一个属性，也**不允许**添加未在注解中声明的额外属性。

这种严格的检查将大量潜在的错误（如属性拼写错误、类型不匹配、结构不一致）扼杀在了萌芽阶段。

为了提高代码的可读性和复用性，我们通常不会直接在变量旁进行内联注解，而是使用上一篇文章中提到的 **类型别名 (Type Alias)** 或 **接口 (Interface)** 来定义对象形状：

```typescript
type User = {
  id: number;
  username: string;
  email: string;
};

let user: User = {
  id: 1,
  username: "alice",
  email: "alice@example.com",
};
```
这种方式更为清晰和模块化，是实际项目中的最佳实践。

### 赋予灵活性：可选属性 (Optional Properties)

严格的结构是好事，但现实世界的数据并非总是如此规整。有时，一个对象的某些属性并不是必需的。例如，一个用户可能有昵称 `nickname`，也可能没有。

为了处理这种情况，TypeScript 提供了**可选属性**，语法是在属性名和冒号之间添加一个问号 `?`。

```typescript
type UserProfile = {
  userId: number;
  nickname?: string; // nickname 是可选的
  bio?: string;      // bio 也是可选的
};

const profile1: UserProfile = { userId: 101, nickname: "The Coder" }; // 合法
const profile2: UserProfile = { userId: 102, bio: "Loves TypeScript." }; // 合法
const profile3: UserProfile = { userId: 103 }; // 合法

// console.log(profile3.nickname.toUpperCase()); // 编译时错误!
// 'profile3.nickname' is possibly 'undefined'.
```
当一个属性被标记为可选时，TypeScript 会推断出它的类型是 `T | undefined`（例如，`nickname` 的类型是 `string | undefined`）。这意味着，当我们在访问这个属性之前，必须先检查它是否存在，否则编译器会发出警告，防止我们对一个 `undefined` 的值执行操作。

这种机制强制我们编写更安全、更严谨的代码来处理可能缺失的数据。

### 保证不可变性：只读属性 (Read-only Properties)

另一类常见需求是，对象的某些属性在被创建后就不应该再被修改。例如，用户的 `id` 或订单的创建时间 `createdAt`。

TypeScript 提供了 `readonly` 关键字来实现这一目标。它可以放在属性名的前面，用来将该属性标记为只读。

```typescript
type Product = {
  readonly id: string; // id 是只读的
  name: string;
  price: number;
};

const book: Product = {
  id: "abc-123",
  name: "Learning TypeScript",
  price: 29.99,
};

// 尝试修改只读属性
// book.id = "def-456"; // 编译时错误!
// Cannot assign to 'id' because it is a read-only property.

// 修改可写属性是允许的
book.name = "Mastering TypeScript"; // 合法
```

一旦一个 `readonly` 属性被初始化赋值，任何后续的修改尝试都会被 TypeScript 编译器拦截。

需要特别强调的是，`readonly` 是一个**编译时**的特性。它只在 TypeScript 的静态类型检查阶段生效，并不会在编译生成的 JavaScript 代码中添加任何运行时的保护机制（与 `Object.freeze()` 不同）。尽管如此，它在开发阶段对于维护数据的不变性、防止意外修改，起到了至关重要的作用。

### 总结

掌握对象类型是精通 TypeScript 的核心环节。通过精确地定义对象结构，我们能构建出更可靠、更易于维护的应用程序。

*   **基本结构定义**：使用类型注解、`type` 别名或 `interface` 来描述对象的形状，确保属性的完整性和类型正确性。
*   **可选属性 (`?`)**：为对象的结构提供了必要的灵活性，允许某些属性存在或不存在，并强制我们在使用前进行安全检查。
*   **只读属性 (`readonly`)**：在编译阶段保护属性不被修改，是实现数据不变性的重要工具。

结合使用这些特性，我们可以创建出既严谨又灵活的类型定义，让代码能够精准地反映我们所处理的数据模型，从而充分发挥 TypeScript 静态类型检查的威力。

