TypeScript Record类型完全指南:从基础到高级应用

本文深入探讨TypeScript中的Record类型,涵盖其基本语法、与元组的区别、实际应用场景(如强制穷尽性检查、枚举映射、查找表创建),以及如何与Partial、Pick、Readonly等实用类型结合使用的高级技巧。

TypeScript Record类型完全指南

TypeScript的Record类型简化了具有一致值类型的对象结构管理。本指南涵盖了Record的基本知识,包括其定义、语法以及它与元组等其他类型的区别。我们将学习如何在实际场景中定义和使用Record,例如强制穷尽性案例处理和映射枚举。此外,我们还将通过将Record与Partial、Pick和Readonly等实用类型结合使用来探索其高级用法。

引言

Record类型是一种实用类型,允许我们创建具有指定键和统一值类型的对象类型。这种类型特别适用于定义映射并确保对象中的所有值都符合单一类型。

Record类型的定义

TypeScript文档中的官方定义是:

1
Record<Keys, Type>

其中:

  • Keys表示记录中的键集,可以是字符串字面量的联合或从联合派生的类型
  • Type是与这些键关联的值的类型

例如,Record<string, number>定义了一个对象,其中每个键都是字符串,每个值都是数字。这种类型确保对象的所有属性都具有相同的值类型,但键可以变化。

Record与元组的比较

Record和元组都用于处理数据集合,但它们有不同的用途。即使它们存储多个值,它们在结构和用法上也有所不同。Record具有具有固定类型的命名属性,而元组是由其位置标识的有序元素列表。以下是一个简单的比较:

  • Record:创建一个对象类型,其中所有值都具有相同的类型,但键可以灵活。这对于将键映射到值并确保所有键都符合特定类型非常有用
  • 元组:定义一个具有固定数量元素的数组,其中每个元素可以具有不同的类型。当我们需要具有每个位置特定类型的固定大小集合时使用元组

例如,考虑以下情况:

这是一个Record类型,它将字符串键映射到数字值:

1
type AgeMap = Record<string, number>;

元组类型表示具有固定位置的字符串(名称)和数字(年龄)的数组:

1
type Person = [string, number];

Record类型的基本用法

Record类型提供了一种简单有效的方法来将键映射到值。当我们需要定义具有特定键值对的对象时,它特别有用,其中键是特定类型,值是另一种类型。

以下是一些使用Record类型定义和创建结构化数据的基本方法。

定义Record

要定义Record,我们指定键和值的类型。下面的示例定义了一个对象,其中每个键都是字符串,每个值也是字符串。这可以用于用户数据的通用映射:

1
type User = Record<string, string>;

创建Record类型示例

一些网站有各种子域。假设这些子域中的每一个都需要某种级别的管理员访问权限,并创建一个Record类型来存储不同的管理员角色及其相应的访问级别。这里,UserRoles和UserStatus是Record类型,其中键是特定的字符串字面量(admin、blogAdmin、docsAdmin、active、inactive、suspended),值是描述每个角色和状态的字符串。

首先,我们定义一个Record类型UserRoles,其中特定的管理员角色作为键,它们的描述作为值。UserRoles类型确保此类型的任何对象都将具有键admin、blogAdmin和docsAdmin,并具有描述每个角色的字符串值。roles对象通过为每个管理员角色提供描述来遵循此类型:

1
2
3
4
5
6
7
type UserRoles = Record<'admin' | 'blogAdmin' | 'docsAdmin', string>;

const roles: UserRoles = {
  admin: 'General Administrator with access to all areas.',
  blogAdmin: 'Administrator with access to blog content.',
  docsAdmin: 'Administrator with access to documentation.'
};

接下来,我们定义一个Record类型UserStatus,其中特定的状态作为键,它们的描述作为值。UserStatus类型确保此类型的任何对象都将具有键active、inactive和suspended,并具有描述每个状态的字符串值。userStatus对象通过为每个状态提供描述来遵循此类型:

1
2
3
4
5
6
7
type UserStatus = Record<'active' | 'inactive' | 'suspended', string>;

const userStatus: UserStatus = {
  active: 'User is currently active and can use all features.',
  inactive: 'User is currently inactive and cannot access their account.',
  suspended: 'User account is suspended due to policy violations.'
};

通过以这种方式创建Record类型,我们确保管理员角色和用户状态在整个应用程序中得到良好定义和一致。

Record类型的实际用例

在本节中,我们将回顾Record类型的几个实际用例,以展示其在不同场景中的多功能性和有效性。

用例1:强制穷尽性案例处理

使用Record定义案例值和消息之间的映射允许我们显式处理每个可能的案例。这确保所有案例都被覆盖,并且任何缺失的案例都会导致编译时错误。

在下面的示例中,statusMessages是一个Record,其中键是特定的Status值(‘pending’、‘completed’、‘failed’),每个键映射到相应的消息。getStatusMessage函数使用此记录根据status参数返回适当的消息。这种方法保证所有状态都得到正确和一致的处理。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type Status = 'pending' | 'completed' | 'failed';

interface StatusInfo {
  message: string;
  severity: 'low' | 'medium' | 'high';
  retryable: boolean;
}

const statusMessages: Record<Status, StatusInfo> = {
  pending: {
    message: 'Your request is pending.',
    severity: 'medium',
    retryable: true,
  },
  completed: {
    message: 'Your request has been completed.',
    severity: 'low',
    retryable: false,
  },
  failed: {
    message: 'Your request has failed.',
    severity: 'high',
    retryable: true,
  },
};

function getStatusMessage(status: Status): string {
  const info = statusMessages[status];
  return `${info.message} Severity: ${info.severity}, Retryable: ${info.retryable}`;
}

// 请求成功的情况
console.log(getStatusMessage('completed')); // Your request has been completed. Severity: low, Retryable: false

用例2:在使用泛型的应用程序中强制类型检查

TypeScript中的泛型允许灵活和可重用的代码。当与Record结合使用时,泛型可以帮助强制类型检查并确保对象符合特定结构。

通过将泛型与Record一起使用,我们可以创建生成具有特定键集和一致值类型的对象的函数或实用程序。这种方法增强了我们代码库中的类型安全性和可重用性。

在下面的示例中,createRecord函数接受一个键数组和一个值,并返回一个Record,其中每个键映射到提供的值。此函数使用泛型(K表示键,T表示值类型)来确保生成的Record具有正确的结构。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function createRecord<K extends string, T>(keys: K[], value: T): Record<K, T> {
  const record: Partial<Record<K, T>> = {};
  keys.forEach(key => record[key] = value);
  return record as Record<K, T>;
}

interface RoleInfo {
  description: string;
  permissions: string[];
}

const userRoles = createRecord(['admin', 'editor', 'viewer'], {
  description: 'Default role',
  permissions: ['read'],
});

console.log(userRoles);
/*
// 输出:
{
  admin: { description: 'Default role', permissions: ['read'] },
  editor: { description: 'Default role', permissions: ['read'] },
  viewer: { description: 'Default role', permissions: ['read'] }
}
*/

用例3:将枚举映射到数据

使用Record将枚举映射到数据允许我们创建一个查找表,其中每个枚举值与特定信息相关联。这对于基于枚举值配置设置等场景特别有用。

在此示例中,colorHex是一个Record,它将每个Color枚举值映射到其相应的十六进制颜色代码。这种方法提供了一种清晰且类型安全的方式来处理基于枚举值的颜色相关数据。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
enum Color {
  Red = 'RED',
  Green = 'GREEN',
  Blue = 'BLUE',
  Yellow = 'YELLOW'
}

interface ColorInfo {
  hex: string;
  rgb: string;
  complementary: string;
}

const colorHex: Record<Color, ColorInfo> = {
  [Color.Red]: {
    hex: '#FF0000',
    rgb: 'rgb(255, 0, 0)',
    complementary: '#00FFFF',
  },
  [Color.Green]: {
    hex: '#00FF00',
    rgb: 'rgb(0, 255, 0)',
    complementary: '#FF00FF',
  },
  [Color.Blue]: {
    hex: '#0000FF',
    rgb: 'rgb(0, 0, 255)',
    complementary: '#FFFF00',
  },
  [Color.Yellow]: {
    hex: '#FFFF00',
    rgb: 'rgb(255, 255, 0)',
    complementary: '#0000FF',
  },
};

console.log(colorHex[Color.Green]); // 输出:{ hex: '#00FF00', rgb: 'rgb(0, 255, 0)', complementary: '#FF00FF' }

用例4:创建查找表

使用Record的查找表有助于将键(如标识符、名称)映射到特定值(如描述、代码)。这对于各种应用程序非常有用,包括配置、翻译和许多其他事情。

这里,countryCode是一个Record,它将国家代码映射到各自的国家名称、人口、首都和大陆。此查找表允许基于国家代码快速且类型安全地检索国家名称和人口。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
type CountryCode = "US" | "CA" | "MX" | "JP";

interface CountryInfo {
  name: string;
  population: number;
  capital: string;
  continent: string;
}

const countryLookup: Record<CountryCode, CountryInfo> = {
  US: {
    name: "United States",
    population: 331000000,
    capital: "Washington D.C.",
    continent: "North America",
  },
  CA: {
    name: "Canada",
    population: 37700000,
    capital: "Ottawa",
    continent: "North America",
  },
  MX: {
    name: "Mexico",
    population: 128000000,
    capital: "Mexico City",
    continent: "North America",
  },
  JP: {
    name: "Japan",
    population: 126300000,
    capital: "Tokyo",
    continent: "Asia",
  },
};

console.log(countryLookup.US);
/*
// 输出:
{
  name: "United States",
  population: 331000000,
  capital: "Washington D.C.",
  continent: "North America"
}
*/

console.log(countryLookup.US.population);// 输出:331000000

迭代Record类型

迭代Record类型对于访问和操作数据结构中的数据非常重要。让我们创建一些示例数据,并展示如何迭代TypeScript Record类型的各种方法。

示例数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Course {
  professor: string;
  credits: number;
  students: string[];
}

interface Courses {
  [key: string]: Course;
}

const courses: Courses = {
  Math101: {
    professor: "Dr. Eze",
    credits: 3,
    students: ["Emmanuel", "Bob", "Charlie"],
  },
  History201: {
    professor: "Dr. Jones",
    credits: 4,
    students: ["Dave", "Eve"],
  },
};

使用forEach

要使用forEach与Record,将其转换为键值对数组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Object.entries(courses).forEach(([key, value]) => {
  console.log(`${key}: ${value.professor}, ${value.credits}`);
  value.students.forEach(student => {
    console.log(`Student: ${student}`);
  });
});

/*
// 输出:
Math101: Dr. Eze, 3
Student: Emmanuel
Student: Bob
Student: Charlie
History201: Dr. Jones, 4
Student: Dave
Student: Eve
*/

使用for…in

for…in循环迭代Record的键:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
for (const key in courses) {
  if (courses.hasOwnProperty(key)) {
    const course = courses[key];
    console.log(`${key}: ${course.professor}, ${course.credits}`);
    course.students.forEach(student => {
      console.log(`Student: ${student}`);
    });
  }
}

/*
// 输出:
Math101: Dr. Eze, 3
Student: Emmanuel
Student: Bob
Student: Charlie
History201: Dr. Jones, 4
Student: Dave
Student: Eve
*/

使用Object.keys()

Object.keys()返回Record键的数组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Object.keys(courses).forEach((key) => {
  const course = courses[key];
  console.log(`${key}: ${course.professor}, ${course.credits}`);
  course.students.forEach(student => {
    console.log(`Student: ${student}`);
  });
});

/*
// 输出:
Math101: Dr. Eze, 3
Student: Emmanuel
Student: Bob
Student: Charlie
History201: Dr. Jones, 4
Student: Dave
Student: Eve
*/

使用Object.values()

Object.values()返回Record值的数组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Object.values(courses).forEach((course) => {
  console.log(`${course.professor}, ${course.credits}`);
  course.students.forEach(student => {
    console.log(`Student: ${student}`);
  });
});

/*
// 输出:
Dr. Eze, 3
Student: Emmanuel
Student: Bob
Student: Charlie
Dr. Jones, 4
Student: Dave
Student: Eve
*/

Record的高级用法和实用类型

Record类型可以与其他实用类型结合使用,以实现更大的灵活性和类型安全性。本节介绍了高级使用模式,展示了Record如何与Pick、Readonly和Partial等实用类型一起工作。

将Record与Pick结合用于选择性类型映射

Pick实用类型允许我们通过从现有类型中选择特定属性来创建新类型。当我们只想使用较大类型中的属性子集时,这非常有用。

在这里,我们通过从ProductInfo接口中仅选择name和price属性创建了一个新类型SelectedProductInfo,然后使用Record将不同的产品映射到此新类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
interface ProductInfo {
  name: string;
  price: number;
  category: string;
}
type SelectedProductInfo = Pick<ProductInfo, "name" | "price">;
type Product = 'Laptop' | 'Smartphone' | 'Tablet';

const products: Record<Product, SelectedProductInfo> = {
  "Laptop": { name: "Dell XPS 15", price: 1500 },
  "Smartphone": { name: "iPhone 12", price: 999 },
  "Tablet": { name: "iPad Pro", price: 799 }
};

将Record与Readonly结合用于不可变属性

Readonly实用类型确保属性在设置后不能被修改。这对于创建不可变数据结构非常有用。

下面示例中的ReadonlyProductInfo类型使ProductInfo的所有属性都不可变,确保每个产品的详细信息在定义后不能被更改:

1
2
3
4
5
6
type ReadonlyProductInfo = Readonly<ProductInfo>;
const readonlyProducts: Record<Product, ReadonlyProductInfo> = {
  "Laptop": { name: "Dell XPS 15", price: 1500, category: "Electronics" },
  "Smartphone": { name: "iPhone 12", price: 999, category: "Electronics" },
  "Tablet": { name: "iPad Pro", price: 799, category: "Electronics" }
};

将Record与Partial结合用于可选属性

Partial实用类型使类型的所有属性都变为可选。这对于可能不知道或不需要所有属性的场景非常有用。

这里,PartialProductInfo类型允许我们创建具有ProductInfo中定义的一些或没有属性的产品,提供了指定产品信息方式的灵活性:

1
2
3
4
5
6
type PartialProductInfo = Partial<ProductInfo>;
const partialProducts: Record<Product, PartialProductInfo> = {
  "Laptop": { name: "Dell XPS 15" },
  "Smartphone": { price: 999 },
  "Tablet": {}
};

将Record与Record结合用于嵌套映射

另一个高级用法涉及组合Record类型来创建嵌套映射,这对于管理复杂数据结构特别有用。

在此示例中,storeInventory使用嵌套Record类型将部门映射到其各自的产品和详细信息,展示了如何组合Record以进行更复杂的数据管理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Department = 'Electronics' | 'Furniture';
type ProductDetails = Record<Product, ProductInfo>;

const storeInventory: Record<Department, ProductDetails> = {
  "Electronics": {
    "Laptop": { name: "Dell XPS 15", price: 1500, category: "Electronics" },
    "Smartphone": { name: "iPhone 12", price: 999, category: "Electronics" },
    "Tablet": { name: "iPad Pro", price: 799, category: "Electronics" }
  },
  "Furniture": {
    "Chair": { name: "Office Chair", price: 200, category: "Furniture" },
    "Table": { name: "Dining Table", price: 500, category: "Furniture" },
    "Sofa": { name: "Living Room Sofa", price: 800, category: "Furniture" }
  }
};

结论

Record类型是管理和构建对象类型的多功能工具,因为它允许我们定义键和值之间的清晰映射,确保代码中的类型安全性和一致性。

有关更详细的信息,请参阅TypeScript文档,并查看其他附加资源,如Total TypeScript和TypeScript Tutorial,以加深对TypeScript Record类型系统的理解。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计