10.对象类型 副本.md 来源:https://my.feishu.cn/file/SmIHbH8FzotZw3x6HugcsXGBnsc 采集状态:已完成正文采集;已从查看代码模式逐行提取,行号 1 至 120 连续,并核对滚动到底。 对象是 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 静态类型检查的威力。