TypeScript Record 类型:从入门到精通
TypeScript 提供了许多强大的工具来帮助我们构建类型安全且可维护的应用程序。其中一个非常有用但有时被低估的工具是 Record 类型。它允许你创建具有特定键类型和值类型的新对象类型。本文将深入探讨 Record 类型,从基础知识到高级用法,帮助你彻底掌握它。
1. 什么是 Record 类型?
在 TypeScript 中,Record<Keys, Type> 是一个泛型工具类型,它构造一个对象类型,其属性键是 Keys 类型,属性值是 Type 类型。
简单来说,如果你想创建一个对象,其中所有键都来自一个特定的联合类型(例如字符串字面量或枚举),并且所有值都具有相同的类型,那么 Record 是一个完美的选择。
基本语法:
“`typescript
type MyRecord
};
“`
从这个定义可以看出,Record 实际上是 TypeScript 映射类型的一种特殊应用。它遍历 Keys 中的每一个成员,并为它们分配 Type 类型。
2. 入门:基本用法
让我们从一些简单的例子开始。
2.1 简单的键和值类型
假设我们想要定义一个对象,它存储不同城市的人口数据,其中城市名是字符串,人口是数字。
“`typescript
type CityPopulation = Record
const population: CityPopulation = {
“New York”: 8000000,
“Los Angeles”: 4000000,
“Chicago”: 2700000,
};
// 正确:键是字符串,值是数字
population[“Houston”] = 2300000;
// 错误:值不是数字
// population[“Phoenix”] = “1600000”; // Type ‘string’ is not assignable to type ‘number’.
“`
在这个例子中,Record<string, number> 告诉 TypeScript population 对象的所有键都必须是 string 类型,所有值都必须是 number 类型。
2.2 使用联合类型作为键
Record 类型真正的威力在于当你使用联合类型作为其键类型时。这允许你定义一个对象,它必须包含来自特定集合的所有属性。
假设我们有一个固定的 LogLevel 集合,我们想为每个级别分配一个颜色。
“`typescript
type LogLevel = “info” | “warn” | “error” | “debug”;
type LogLevelColors = Record
const colors: LogLevelColors = {
info: “blue”,
warn: “orange”,
error: “red”,
debug: “gray”,
};
// 正确:所有 LogLevel 的键都被包含
console.log(colors.info); // “blue”
// 错误:缺少 ‘debug’ 属性
/
const partialColors: LogLevelColors = {
info: “blue”,
warn: “orange”,
error: “red”,
};
// Property ‘debug’ is missing in type ‘{ info: “blue”; warn: “orange”; error: “red”; }’
// but required in type ‘Record
// 错误:不允许未定义的键
/
const invalidColors: LogLevelColors = {
info: “blue”,
warn: “orange”,
error: “red”,
debug: “gray”,
fatal: “black”, // Object literal may only specify known properties, and ‘fatal’ does not exist in type ‘Record
};
“`
这展示了 Record 类型如何强制执行一个严格的结构:对象必须包含 LogLevel 联合类型中的所有键,并且不能包含其他键。
2.3 使用枚举作为键
枚举是创建一组命名常量的好方法,它们也非常适合作为 Record 的键。
“`typescript
enum UserRole {
Admin = “admin”,
Editor = “editor”,
Viewer = “viewer”,
}
type RolePermissions = Record
const permissions: RolePermissions = {
};
console.log(permissionsUserRole.Admin); // true
// 错误:缺少 UserRole.Viewer
/*
const incompletePermissions: RolePermissions = {
};
// Property ‘viewer’ is missing in type ‘{ admin: true; editor: true; }’
// but required in type ‘Record
*/
“`
3. 进阶用法
Record 类型可以与其他 TypeScript 类型和工具类型结合使用,以实现更复杂的类型定义。
3.1 Record 与可选属性
有时,你可能希望 Record 中的某些属性是可选的。你可以通过结合 Partial 工具类型来实现这一点。
“`typescript
type UserSettingsKey = “theme” | “notifications” | “language”;
type UserSettings = Partial
const user1Settings: UserSettings = {
theme: “dark”,
language: “en-US”,
};
const user2Settings: UserSettings = {
notifications: “email”,
};
const user3Settings: UserSettings = {}; // 所有都是可选的,所以空对象也是允许的
“`
Partial<Record<UserSettingsKey, string>> 创建了一个类型,其中 UserSettingsKey 中的所有键都是可选的,并且它们的值都是 string 类型。
3.2 Record 与只读属性
如果你想让 Record 中的所有属性都是只读的,可以使用 Readonly 工具类型。
“`typescript
type ConfigKey = “apiEndpoint” | “timeout” | “maxRetries”;
type AppConfig = Readonly
const appConfig: AppConfig = {
apiEndpoint: “https://api.example.com”,
timeout: 5000,
maxRetries: 3,
};
// 错误:无法分配到 “apiEndpoint”,因为它是一个只读属性
// appConfig.apiEndpoint = “https://new-api.example.com”;
“`
3.3 嵌套 Record
你可以创建嵌套的 Record 类型来表示更复杂的数据结构。
“`typescript
type ProductCategory = “electronics” | “books” | “clothing”;
type ProductSubCategory = “laptops” | “smartphones” | “fiction” | “non-fiction” | “t-shirts” | “jeans”;
type ProductCatalog = Record<
ProductCategory,
Record
;
const catalog: ProductCatalog = {
electronics: {
laptops: [“MacBook Pro”, “Dell XPS”],
smartphones: [“iPhone 15”, “Samsung Galaxy S24”],
// 允许有其他 ProductSubCategory 的键,只要类型匹配
fiction: [], // TypeScript 允许在此处有空数组,但逻辑上可能不合理
“non-fiction”: [],
“t-shirts”: [],
jeans: []
},
books: {
fiction: [“Dune”, “1984”],
“non-fiction”: [“Sapiens”, “Cosmos”],
laptops: [],
smartphones: [],
“t-shirts”: [],
jeans: []
},
clothing: {
“t-shirts”: [“Graphic Tee”, “Polo Shirt”],
jeans: [“Slim Fit Jeans”, “Bootcut Jeans”],
laptops: [],
smartphones: [],
fiction: [],
“non-fiction”: []
},
};
console.log(catalog.electronics.laptops); // [“MacBook Pro”, “Dell XPS”]
// 注意:由于 ProductSubCategory 是一个大的联合类型,你必须为每个 ProductCategory 提供所有 ProductSubCategory 的键,
// 即使它们不相关。这可能会导致类型定义变得臃肿。
// 在这种情况下,可能需要重新考虑数据结构或使用更复杂的映射类型。
**注意:** 上述嵌套 `Record` 的例子展示了一个潜在的缺点:如果 `ProductSubCategory` 包含很多不适用于所有 `ProductCategory` 的键,你将不得不为不相关的键提供空数组或其他默认值,这可能会导致数据结构变得不那么优雅。在这种情况下,考虑使用更具体的映射类型或不同的数据建模方法可能更合适,例如:typescript
type ProductMap = {
electronics: { laptops: string[], smartphones: string[] };
books: { fiction: string[], “non-fiction”: string[] };
clothing: { “t-shirts”: string[], jeans: string[] };
};
const smarterCatalog: ProductMap = {
electronics: {
laptops: [“MacBook Pro”, “Dell XPS”],
smartphones: [“iPhone 15”, “Samsung Galaxy S24”],
},
books: {
fiction: [“Dune”, “1984”],
“non-fiction”: [“Sapiens”, “Cosmos”],
},
clothing: {
“t-shirts”: [“Graphic Tee”, “Polo Shirt”],
jeans: [“Slim Fit Jeans”, “Bootcut Jeans”],
},
};
“`
这种方式虽然更冗长,但在某些场景下能提供更精确的类型检查。
3.4 Record 与 unknown 或 any
在某些情况下,你可能不知道 Record 中值的具体类型,或者你打算稍后进行更具体的类型检查。
“`typescript
type DynamicData = Record
const data: DynamicData = {
id: 1,
name: “Alice”,
details: {
email: “[email protected]”,
isActive: true,
},
tags: [“user”, “premium”],
};
// 你可以在运行时对 unknown 类型进行类型缩小
if (typeof data.id === ‘number’) {
console.log(data.id.toFixed(2));
}
“`
使用 unknown 比使用 any 更安全,因为它强制你在访问属性之前进行类型检查。
4. Record 与接口 (Interface) 和类型别名 (Type Alias) 的对比
Record、接口和类型别名在定义对象结构方面都有用,但它们有不同的用例。
- 接口 (Interface):主要用于描述对象的“形状”,支持声明合并,常用于定义公共 API 和类实现。
typescript
interface User {
id: number;
name: string;
} - 类型别名 (Type Alias):可以为任何类型创建别名,包括联合类型、交叉类型、基本类型和对象类型。
typescript
type Product = {
id: string;
price: number;
}; Record类型:特别适用于当你需要一个对象,其所有键都来自一个已知的集合(通常是联合类型或枚举),并且所有值都具有相同类型时。它强制对象包含所有这些键,并且不允许包含其他键。
何时使用 Record?
当你需要一个字典或映射表,其中:
1. 键的集合是预先知道且有限的(例如,一组字符串字面量、枚举成员)。
2. 所有值都共享相同的类型。
3. 你希望 TypeScript 强制该对象必须包含所有定义的键。
如果你的对象键不固定,或者每个键的值类型不同,那么传统的接口或类型别名可能更合适。
5. 最佳实践和注意事项
- 明确键类型: 尽可能使用具体的字符串字面量联合类型或枚举作为
Record的键类型,而不是宽泛的string。这能提供更强的类型安全和更好的自动补全。 - 避免过度使用
Record<string, any>: 这会削弱 TypeScript 的类型检查能力。如果可能,使用unknown并进行类型缩小,或者定义更具体的类型结构。 - 考虑数据结构: 如果
Record的键集合非常大且稀疏(即许多键的值会是默认或空),可能需要重新评估你的数据结构设计。有时,一个包含固定属性的对象数组或更复杂的映射类型可能更合适。 -
与
keyof和typeof结合: 你可以使用keyof运算符来提取现有对象的键类型,并结合Record来创建新的类型。
“`typescript
const defaultSettings = {
theme: “light”,
notificationsEnabled: true,
};type SettingsKeys = keyof typeof defaultSettings; // “theme” | “notificationsEnabled”
type UserPreferences = Record; const userPref: UserPreferences = {
theme: “dark”,
notificationsEnabled: false,
};
“`
总结
TypeScript 的 Record 类型是一个强大且灵活的工具,特别适用于创建具有统一键和值类型的对象。通过理解其基本用法和如何与其他工具类型结合,你可以构建出更健壮、更具表现力的 TypeScript 代码。它在处理配置对象、状态映射、字典数据结构等场景中发挥着重要作用。熟练掌握 Record,将使你在 TypeScript 的世界中如虎添翼。