在计算机科学中,记录(Record)是一种包含多个字段的数据结构,这些字段可能有不同的类型。在 TypeScript 中,Record 类型让我们可以定义字典(又称为键值对),其中键和值都有固定的类型。
简单来说,Record 类型让我们可以定义字典的类型,也就是它的键和值的类型。在这篇文章中,我们将深入探讨 TypeScript 中的 Record 类型,了解它是什么以及如何使用它。我们还会研究如何使用它进行枚举处理,以及如何将其与泛型结合使用,以便在编写可重用代码时了解返回值的属性。
Record 与 Tuple 的区别是什么?
在 TypeScript 中,Record 类型和 Tuple 类型虽然都有固定的成员数量,但它们在使用和定义上有明显的区别。理解这些区别可以帮助我们更有效地选择合适的数据结构来编写更加简洁和可维护的代码。
Record<Keys, Type> 构造了一个对象类型,该对象的属性键为 Keys,属性值为 Type。这种工具类型可以用于将一种类型的属性映射到另一种类型。
换句话说,Record 类型允许我们定义一个对象,其中键和值都有固定的类型。键通常是字符串或数字,值可以是任何类型。
Tuple(元组)则是一个有序元素的集合,每个元素的位置在定义时是固定的。与 Record 类型不同,Tuple 中的元素是通过它们在元组中的位置来标识的,而不是通过名称。
Record 与 Tuple 的主要区别
成员标识:Record 类型的成员通过名称标识,而 Tuple 类型的成员通过位置标识。
用途:Record 类型更适合用于描述具有命名字段的对象,而 Tuple 类型更适合用于描述有固定顺序和数量的集合。
灵活性:Record 类型的字段顺序无关紧要,可以通过名称访问任意字段;Tuple 类型的元素顺序固定,必须通过位置访问。
TypeScript 的 Record 类型
TypeScript 的 Record 类型的强大之处在于我们可以用它来创建具有固定键数的字典。例如,我们可以使用 Record 类型为大学课程创建一个模型:
type Course = "Computer Science" | "Mathematics" | "Literature"
interface CourseInfo {
professor: string
cfu: number
}
const courses: Record<Course, CourseInfo> = {
"Computer Science": {
professor: "Mary Jane",
cfu: 12
},
"Mathematics": {
professor: "John Doe",
cfu: 12
},
"Literature": {
professor: "Frank Purple",
cfu: 12
}
}
在这个例子中,我们定义了一个名为 Course
的类型,用于列出课程名称,以及一个名为 CourseInfo
的类型,用于保存课程的一些基本信息。然后,我们使用 Record 类型将每个 Course
与其 CourseInfo
进行匹配。
检测缺失的属性
TypeScript 的 Record 类型不仅让我们创建字典,还能帮助我们在编译时检测到遗漏的课程。例如,如果我们没有包括 Literature ,就会在编译时收到如下错误:
“Property Literature
is missing in type { "Computer Science": { professor: string; cfu: number; }; Mathematics: { professor: string; cfu: number; }; }
but required in type Record<Course, CourseInfo>
.”
在这个例子中,TypeScript 明确告诉我们缺少了文学课程。
检测未定义的属性
同样地,如果我们添加了一个未在 Course
类型中定义的课程,比如History
课程,TypeScript 也会检测到并给出错误:
“Object literal may only specify known properties, and "History"
does not exist in type Record<Course, CourseInfo>
.”
这样可以确保我们只定义了在 Course
类型中列出的课程。
访问 Record 数据
我们可以像访问普通字典一样访问每个课程的相关数据:
console.log(courses["Literature"])
上面的代码将输出以下内容:
{
"professor": "Frank Purple",
"cfu": 12
}
用例 1:强制穷尽情况处理
在编写现代应用程序时,通常需要根据某些区分值运行不同的逻辑。例如,在工厂设计模式中,我们根据输入创建不同对象的实例。在这种情况下,处理所有情况至关重要。
使用 switch 语句的简单解决方案
最简单(但有些天真的)解决方案可能是使用 switch 结构来处理所有情况:
type Discriminator = 1 | 2 | 3;
function factory(d: Discriminator): string {
switch(d) {
case 1:
return "1";
case 2:
return "2";
case 3:
return "3";
default:
return "0";
}
}
然而,如果我们向 Discriminator
添加一个新值,由于有默认分支,TypeScript 不会告诉我们工厂函数中未处理新的情况。如果没有默认分支,TypeScript 将检测到 Discriminator
中添加了新值。
利用 Record 类型解决问题
我们可以利用 Record 类型的强大功能来解决这个问题:
type Discriminator = 1 | 2 | 3;
function factory(d: Discriminator): string {
const factories: Record<Discriminator, () => string> = {
1: () => "1",
2: () => "2",
3: () => "3"
};
return factories[d]();
}
console.log(factory(1));
新的工厂函数定义了一个 Record,将 Discriminator
与特定的初始化函数匹配,这些函数不需要输入参数并返回一个字符串。然后,工厂函数根据 d: Discriminator
获取正确的函数,并通过调用结果函数返回一个字符串。如果现在向 Discriminator
添加更多元素,Record 类型将确保 TypeScript 检测到 factories
中缺少的情况。
用例 2:在使用泛型的应用程序中强制类型检查
泛型允许我们编写对实际类型抽象的代码。例如,Record<K, V>
是一种泛型类型。当我们使用它时,必须为键(K)和值(V)选择两种实际类型。
泛型在现代编程中非常有用,因为它们使我们能够编写高度可重用的代码。HTTP 调用或数据库查询的代码通常是针对返回值的类型进行泛型化的。这非常好,但也带来了一些挑战,因为我们很难知道返回值的实际属性。
我们可以通过利用 Record 类型来解决这个问题:
class Result<Properties = Record<string, any>> {
constructor(
public readonly properties: Record<
keyof Properties,
Properties[keyof Properties]
>
) {}
}
Result
类看起来有点复杂。在这个例子中,我们将其声明为一个泛型类型,其中类型参数 Properties
默认为 Record<string, any>
。
使用 any
可能看起来不太理想,但这是有道理的。正如我们稍后将看到的,Record 将属性名称映射到属性值,因此我们无法提前知道属性的类型。此外,为了使其尽可能可重用,我们必须使用 TypeScript 中最抽象的类型——确实是 any
!
构造函数利用 TypeScript 的语法糖来定义一个只读属性,我们将其命名为 properties
。注意 Record 类型的定义:
键的类型是
keyof Properties
,这意味着每个对象的键必须与Properties
泛型类型中定义的键相同。每个键的值将是
Properties
记录中相应属性的值。
实验和使用示例
现在我们已经定义了主要的封装类型,可以通过以下示例来实验并使用 Result
类,以展示如何使用它来进行类型检查:
interface CourseInfo {
title: string;
professor: string;
cfu: number;
}
const course = new Result<CourseInfo>({
title: "文学",
professor: "Mary Jane",
cfu: 12
});
console.log(course.properties.title);
//console.log(course.properties.students); // 这一行将无法编译!
在上面的代码中,我们定义了一个 CourseInfo
接口,模型化我们想要存储和查询的基本信息:课程名称、教授的姓名和学分数。
接下来,我们模拟创建一个课程。这只是一个字面值,但你可以想象它是数据库查询或 HTTP 调用的结果。
注意,我们可以以类型安全的方式访问课程属性。当我们引用现有属性(如 title
)时,它可以编译并按预期工作。当我们尝试访问不存在的属性(如 students
)时,TypeScript 检测到 CourseInfo
声明中缺少该属性,因此无法编译。
这是一个强大的功能,我们可以在代码中利用它来确保从外部来源获取的值符合预期的属性集合。请注意,如果 course
拥有比 CourseInfo
定义的更多的属性,我们仍然可以访问它们。换句话说,以下代码片段是可以工作的:
// `CourseInfo` 和 `Result` 如上所述
const course = new Result<CourseInfo & Record<string, any>>({
title: "文学",
professor: "Mary Jane",
cfu: 12,
webpage: "https://..."
});
console.log(course.properties.webpage); // 输出: "https://..."
Record 类型 vs. Map 类型
在 TypeScript 中,Record 类型和 Map 类型都是用于存储键值对的有用工具,但它们有不同的特性和用例。下面我们将通过几个方面来比较 TypeScript 的 Record 和 Map 类型。
性能
Record 类型:Record 类型适用于静态键,并且在访问值时通常速度更快,因为键是预定义的,编译器可以对其进行优化。
Map 类型:Map 类型针对动态数据进行了优化,适用于键类型可能在运行时变化的场景。由于其灵活性,键查找可能比 Record 稍慢。
类型安全
Record 类型:Record 类型对键和值都有强类型安全。在编译时,TypeScript 确保键和值符合指定的类型。这使得代码更加安全和可维护。
Map 类型:Map 类型允许任意类型的键,因此类型安全性较 Record 类型稍弱。不过,Map 类型对值仍提供一定的类型安全。
使用场景
Record 类型:适用于类型安全至关重要且键已知的场景,例如配置对象或枚举类型的映射。
Map 类型:适用于需要灵活键类型的场景,例如在运行时动态生成键或需要对象、函数等非字符串类型的键。此外,Map 保留了插入顺序,因此适用于需要维护元素顺序的场景。
语法
Record 类型:使用
Record<Key, Value>
语法定义。Map 类型:使用
new Map<Key, Value>()
语法定义。
键类型
Record 类型:键类型固定且有限,通常为字符串和数字。这使得 Record 类型在处理预定义键时非常高效。
Map 类型:允许任意类型的键,灵活性更高,可以使用字符串、数字、对象、函数等作为键。这使得 Map 类型在处理动态生成键或非字符串类型的键时更加适用。
总结
Record 类型:适用于需要高类型安全性和固定键的场景。它在编译时提供强类型检查,并且性能上对于静态键非常高效。
Map 类型:适用于需要灵活键类型和保持插入顺序的场景。它允许任意类型的键,适用于更动态和复杂的数据结构。
遍历 TypeScript Record 类型的方法
在本节中,我们将探索多种遍历 Record 类型的方法,包括 forEach
、for...in
、Object.keys()
和 Object.values()
。了解如何遍历 TypeScript Record 类型对于有效访问这些结构中的数据至关重要。
使用 forEach
要使用 forEach
遍历 Record 类型,首先需要将 Record 转换为键值对数组。这可以使用 Object.entries()
方法实现:
type Course = "Computer Science" | "Mathematics" | "Literature";
interface CourseInfo {
professor: string;
cfu: number;
}
const courses: Record<Course, CourseInfo> = {
"Computer Science": { professor: "Mary Jane", cfu: 12 },
"Mathematics": { professor: "John Doe", cfu: 12 },
"Literature": { professor: "Frank Purple", cfu: 12 },
};
Object.entries(courses).forEach(([key, value]) => {
console.log(`${key}: ${value.professor}, ${value.cfu}`);
});
使用 for...in
for...in
循环允许遍历 Record 的键:
for (const key in courses) {
if (courses.hasOwnProperty(key)) {
const course = courses[key as Course];
console.log(`${key}: ${course.professor}, ${course.cfu}`);
}
}
使用 Object.keys()
Object.keys()
返回 Record 键的数组,然后可以使用 forEach
或其他循环进行遍历:
Object.keys(courses).forEach((key) => {
const course = courses[key as Course];
console.log(`${key}: ${course.professor}, ${course.cfu}`);
});
使用 Object.values()
Object.values()
返回 Record 值的数组,可以对其进行遍历:
Object.values(courses).forEach((course) => {
console.log(`${course.professor}, ${course.cfu}`);
});
使用 Object.entries()
Object.entries()
返回键值对数组,允许在循环中使用数组解构:
Object.entries(courses).forEach(([key, value]) => {
console.log(`${key}: ${value.professor}, ${value.cfu}`);
});
TypeScript Record 类型的高级用例
TypeScript 的 Record 类型不仅可以用于简单的键值对映射,还可以用于更高级的模式,如结合 Pick 类型进行选择性类型映射和实现动态键值对。这些用例为处理复杂数据结构提供了额外的灵活性和控制。
使用 Pick 类型与 Record 进行选择性类型映射
TypeScript 的 Pick 类型允许我们通过选择现有类型中的特定属性来创建新类型。当它与 Record 类型结合使用时,就成为创建仅包含部分属性的字典的强大工具。
假设我们有一个包含多个属性的 CourseInfo
接口,但我们只想在 Record 中映射这些属性的一个子集:
interface CourseInfo {
professor: string;
cfu: number;
semester: string;
students: number;
}
type SelectedCourseInfo = Pick<CourseInfo, "professor" | "cfu">;
type Course = "Computer Science" | "Mathematics" | "Literature";
const courses: Record<Course, SelectedCourseInfo> = {
"Computer Science": { professor: "Mary Jane", cfu: 12 },
"Mathematics": { professor: "John Doe", cfu: 12 },
"Literature": { professor: "Frank Purple", cfu: 12 },
};
在上面的例子中,我们使用 Pick<CourseInfo, "professor" | "cfu">
创建了一个新类型 SelectedCourseInfo
,该类型仅包括 CourseInfo
中的 professor
和 cfu
属性。然后,我们定义了一个 Record 类型,将每个 Course
映射到 SelectedCourseInfo
。
使用 Record 实现动态键值对的字典模式
TypeScript 的 Record 类型还可以用于实现动态键值对,这对于创建键在预先未知的字典非常有用。
考虑一种场景,我们有动态的用户偏好设置,其中的键和值可以随时间变化:
type PreferenceKey = string; // 动态键
type PreferenceValue = string | boolean | number; // 值
interface UserPreferences {
[key: string]: PreferenceValue;
}
const userPreferences: Record<PreferenceKey, PreferenceValue> = {
theme: "dark",
notifications: true,
fontSize: 14,
};
// 更新偏好设置
userPreferences.language = "English";
// 遍历偏好设置
Object.entries(userPreferences).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
在上述代码中,我们将 PreferenceKey
定义为字符串,并将 PreferenceValue
定义为可以是字符串、布尔值或数字的联合类型。UserPreferences
接口使用索引签名来表示具有 PreferenceValue
类型的动态键。然后我们使用 Record<PreferenceKey, PreferenceValue>
创建一个 userPreferences
字典,用于存储和遍历动态用户偏好设置。
结合其他实用类型使用 TypeScript 的 Record 类型
TypeScript 的实用类型可以与 Record 类型结合使用,以创建更复杂和类型安全的数据结构。在本节中,我们将介绍如何将 ReadOnly 和 Partial 类型与 Record 类型结合使用。
使用 ReadOnly 与 Record
ReadOnly
类型使一个类型的所有属性变为只读。这在确保字典条目不能被修改时特别有用:
type ReadonlyCourseInfo = Readonly<CourseInfo>;
const readonlyCourses: Record<Course, ReadonlyCourseInfo> = {
"Computer Science": { professor: "Mary Jane", cfu: 12, semester: "Fall", students: 100 },
"Mathematics": { professor: "John Doe", cfu: 12, semester: "Spring", students: 80 },
"Literature": { professor: "Frank Purple", cfu: 12, semester: "Fall", students: 60 },
};
// Trying to modify a readonly property will result in a compile-time error
// readonlyCourses["Computer Science"].cfu = 14; // Error: Cannot assign to 'cfu' because it is a read-only property.
在上面的代码中,Readonly<CourseInfo>
确保了 CourseInfo
的所有属性都是只读的,防止任何修改。尝试修改只读属性将导致编译时错误:
readonlyCourses["Computer Science"].cfu = 14;
// 错误:不能给 'cfu' 赋值,因为它是只读属性。
使用 Partial 与 Record
Partial
类型使一个类型的所有属性变为可选。这在创建某些条目可能没有定义所有属性的字典时特别有用:
type PartialCourseInfo = Partial<CourseInfo>;
const partialCourses: Record<Course, PartialCourseInfo> = {
"Computer Science": { professor: "Mary Jane" },
"Mathematics": { cfu: 12 },
"Literature": {},
};
总结
在本文中,我们探讨了 TypeScript 的内建类型 Record<K, V>
,包括它的基本用法和行为。然后,我们研究了两个使用 Record 类型的突出用例:
确保我们处理枚举的所有情况。
在应用程序中使用泛型类型对任意对象的属性进行类型检查。
此外,我们介绍了如何使用各种方法遍历 TypeScript 的 Record 类型,如 forEach
、for...in
、Object.keys()
和 Object.values()
,这些方法允许我们有效地操作和访问 Record 类型中的数据。
通过结合 TypeScript 的实用类型,如 Pick
、Partial
和 Readonly
,我们可以与 Record 类型一起创建更复杂和类型安全的数据结构。理解这些高级模式可以增强你使用 TypeScript 的能力。
Record 类型非常强大,虽然它的一些用例比较小众,但它为我们的应用代码提供了极大的价值。如果有任何问题或发现了新的有趣用法,欢迎在评论区分享!