掌握 TypeScript Record:构建强类型对象
在 TypeScript 的类型系统中,工具类型(Utility Types)扮演着至关重要的角色,它们能够帮助我们以声明式的方式操作类型,从而提升代码的健壮性和可维护性。其中,Record<K, T> 是一个非常强大且常用的工具类型,它允许我们构建一个对象类型,其中属性键的类型为 K,属性值的类型为 T。
本文将深入探讨 Record 工具类型,包括其语法、工作原理、常见用例以及与 JavaScript 原生对象和 Map 类型的比较,帮助你更好地掌握如何在项目中构建强类型对象。
1. Record<K, T> 的基本语法与工作原理
Record 的语法非常直观:
typescript
type MyObject = Record<K, T>;
K(Keys): 代表对象属性键的类型。K可以是:string、number或symbol类型。- 字符串字面量联合类型(如
'id' | 'name' | 'age')。 - 枚举类型。
T(Values): 代表对象属性值的类型。T可以是任何 TypeScript 类型,包括基本类型、对象类型、联合类型等。
Record<K, T> 的核心作用是创建一个新的对象类型,该类型强制要求其所有属性的键都必须符合类型 K,并且所有属性的值都必须符合类型 T。这为我们提供了一种在编译时就能确保对象结构一致性的强大机制。
2. 实际应用示例
让我们通过几个例子来深入理解 Record 的用法。
2.1. 键是字符串字面量联合类型
当你知道对象可能包含的特定键集合时,使用字符串字面量联合类型作为 K 是非常合适的。
“`typescript
type UserRole = ‘admin’ | ‘editor’ | ‘viewer’;
type UserPermissions = Record
const rolePermissions: UserPermissions = {
admin: true,
editor: true,
viewer: false,
};
// ❌ 错误:缺少 ‘viewer’ 属性
// const incompletePermissions: UserPermissions = {
// admin: true,
// editor: true,
// };
// ❌ 错误:’moderator’ 不是 UserRole 中的有效键
// const invalidKeyPermissions: UserPermissions = {
// admin: true,
// editor: true,
// viewer: false,
// moderator: true,
// };
console.log(rolePermissions.admin); // true
“`
这个例子中,UserPermissions 类型确保了任何 UserPermissions 类型的对象都必须包含 admin、editor 和 viewer 这三个键,且它们的值必须是 boolean 类型。
2.2. 键是枚举类型
使用枚举类型作为 K 可以增强代码的可读性和维护性,特别是在处理状态或配置时。
“`typescript
enum StatusCode {
Success = 200,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404,
}
type StatusMessages = Record
const httpMessages: StatusMessages = {
};
// ❌ 错误:如果缺少任何一个枚举成员,都会报错
// const incompleteMessages: StatusMessages = {
// StatusCode.Success: ‘OK’,
// };
console.log(httpMessagesStatusCode.NotFound); // 资源未找到
“`
这里,StatusMessages 确保了所有 StatusCode 枚举成员都有一个对应的字符串消息。
2.3. 键是 string 或 number
当键的集合不固定,但你希望所有值的类型都保持一致时,可以使用 string 或 number。
“`typescript
type UserData = Record
const userProfile: UserData = {
name: ‘Alice’,
age: 30,
city: ‘New York’,
};
// ✅ 允许:可以添加新的字符串键,只要值类型符合
userProfile.country = ‘USA’;
// ❌ 错误:值类型不符合 (boolean)
// userProfile.isActive = true;
type Scoreboard = Record
const gameScores: Scoreboard = {
1: { name: ‘Player A’, score: 100 },
2: { name: ‘Player B’, score: 150 },
};
“`
3. Record 的常见用例
Record 工具类型在许多场景下都非常有用:
- 配置对象 (Configuration Objects): 定义应用的设置,确保每个设置项都具有预期的类型。
- 查找表/字典 (Lookup Tables/Dictionaries): 创建一个键值对映射,其中键的集合固定,且值的类型一致。
- API 响应数据结构 (API Response Structuring): 当从 API 获取的数据结构具有已知键和统一值类型时,使用
Record可以很好地描述。 - 多语言支持 (Internationalization – i18n): 为不同的语言键提供翻译文本,确保所有语言键都被覆盖。
- 状态管理 (State Management): 在 Redux 或 Zustand 等状态管理库中定义切片(slice)的状态结构,确保特定部分的状态类型一致。
- 穷举检查 (Exhaustive Checking): 结合字符串字面量联合类型或枚举,
Record可以帮助你确保处理了所有可能的键,从而实现更严格的类型检查和避免运行时错误。
4. Record 与其他对象表示的比较
理解 Record 的最佳方式之一是将其与其他常见的对象表示方法进行比较。
4.1. Record<K, T> vs. 索引签名 ({ [key: string]: T })
在某些情况下,Record<string, T> 看起来与 { [key: string]: T } 相似,但它们之间存在关键差异。
“`typescript
type IndexSignatureType = { [key: string]: number };
type RecordStringType = Record
const obj1: IndexSignatureType = { a: 1, b: 2 }; // ✅ 有效
const obj2: RecordStringType = { a: 1, b: 2 }; // ✅ 有效
// 它们的主要区别在于对键的限制:
type SpecificKeysIndex = { [key: ‘foo’ | ‘bar’]: string }; // ❌ 错误:索引签名键必须是 string 或 number
// type SpecificKeysRecord = Record<‘foo’ | ‘bar’, string>; // ✅ 有效,这正是 Record 的强项
const obj3: SpecificKeysRecord = { foo: ‘hello’, bar: ‘world’ };
// const obj4: SpecificKeysRecord = { foo: ‘hello’ }; // ❌ 错误:缺少 ‘bar’
“`
- 索引签名 (
{ [key: string]: T }): 允许对象拥有任意数量的字符串(或数字)键,只要它们的值符合类型T。它不强制对象包含任何特定的键。 Record<K, T>: 当K是一个字符串字面量联合类型(如'a' | 'b')或枚举时,它会强制对象包含所有这些特定的键。如果K是string或number,则行为与索引签名类似,但Record提供了更清晰的意图表达。
因此,当你想强制对象包含一组明确定义的键时,Record<K, T> (K 为联合类型或枚举) 是更优的选择,因为它提供了更强的类型安全保证。
4.2. Record vs. Map<K, V>
JavaScript 原生的 Map 对象也用于存储键值对,但