掌握 TypeScript 中的映射类型

简介: 掌握 TypeScript 中的映射类型

DRY 原则(Don't repeat yourself)是软件开发中最重要的原则之一,即不要重复自己。应该避免在代码中的两个或多个地方存在重复的业务逻辑。

在 TypeScript 中,映射类型可以帮助我们避免编写重复的代码,它可以根据现有类型和定义的一些规则来创建新类型。下面就来看一下什么是映射类型以及如何构建自己的映射类型。


1. 基本概念


在介绍映射类型之前,先来看一些前置知识。

(1)索引访问类型

在 TypeScript 中,我们可以通过按名称查找属性来访问它的类型:

type AppConfig = {
  username: string;
  layout: string;
};
type Username = AppConfig["username"];

在这个例子中,通过 AppConfig 类型的索引 username 获取到其类型 string,类似于在 JavaScript 中通过索引来获取对象的属性值。

(2)索引签名

当类型属性的实际名称是未知的,但它们将引用的数据类型已知时,索引签名就很方便。


type User = {
  name: string;
  preferences: {
    [key: string]: string;
  }
};
const currentUser: User = {
  name: 'Foo Bar',
  preferences: {
    lang: 'en',
  },
};
const currentLang = currentUser.preferences.lang;


在上面的例子中,currentLang 的类型是 string 而不是 any。此功能与 keyof 运算符一起搭配使用是使映射类型成为可能的核心之一。

(3)联合类型

联合类型是两种或多种类型的组合。 它表明值的类型可以是联合中包含的任何一种类型。

type StringOrNumberUnion = string | number;
let value: StringOrNumberUnion = 'hello, world!';
value = 100;

下面是一个更复杂的例子,编译器可以为联合类型提供一些高级保护:

type Animal = {
  name: string;
  species: string;
};
type Person = {
  name: string;
  age: number;
};
type AnimalOrPerson = Animal | Person;
const value: AnimalOrPerson = loadFromSomewhereElse();
console.log(value.name);   // ✅
console.log(value.age);    // ❌
if ('age' in value) {
  console.log(value.age); // ✅
}

在这个例子中,因为 Animal 和 Person 都有 name 属性,所以第 15 行的 value.name 可以正常输出,没有错误。而第 16 行的 value.age 会编译错误,因为如果 value 是 Animal 类型,则 value 是没有 age 属性的。在第 19 行的 if 块中,因为只有 value 存在 age 属性才能进入这个代码块。所以,在这个 if 块中,value 一定是 Person,TS 可以知道 value 一定是具有 age 属性的,所以编译正确。

(4)keyof 类型运算符

keyof 类型运算符返回传递给它的类型的 key 的联合。


type AppConfig = {
  username: string;
  layout: string;
};
type AppConfigKey = keyof AppConfig;

在这个例子中,AppConfigKey 类型会被解析为"username" | "layout"。它可以与索引签名一起使用:

type User = {
  name: string;
  preferences: {
    [key: string]: string;
  }
};
type UserPreferenceKey = keyof User["preferences"];

这里,UserPreferenceKey 类型被解析为 string | number

(5)元组类型

元组是一种特殊的数组类型,其中数组的元素可能是特定索引处的特定类型。 它们允许 TypeScript 编译器围绕值数组提供更高的安全性,尤其是当这些值属于不同类型时。

例如,TypeScript 编译器能够为元组的各种元素提供类型安全:


type Currency = [number, string];
const amount: Currency = [100, 'USD'];
function add(values: number[]) {
   return values.reduce((a, b) => a + b);
}
add(amount);
// Error: Argument of type 'Currency' is not assignable to parameter of type 'number[]'.
// Type 'string' is not assignable to type 'number'.

上面的代码中会报错,Currency 类型的参数不能分配给“number[]”类型的参数,string 类型不能分配给 number 类型。

当访问超出元组定义类型的索引处的元素时,TypeScript 能够进行提示:

type LatLong = [number, number]; 
const loc: LatLong = [48.858370, 2.294481];
console.log(loc[2]);
// Error: Tuple type 'LatLong' of length '2' has no element at index '2'.

这里,元组类型 LatLong 只有两个元素,当试图访问第三个元素时,就会报错。

(6)条件类型

条件类型是一个表达式,类似于 JavaScript 中的三元表达式,其语法如下:

typescript

复制代码

T extends U ? X : Y

来看一个实际的例子:

typescript

复制代码

typeConditionalType = stringextendsboolean ? string : boolean;

在上面的示例中,ConditionalType 的类型将是 boolean,因为条件string extends boolean 是始终为 false。


2. 映射类型

(1)初体验

在 TypeScript 中,当需要从另一种类型派生(并保持同步)另一种类型时,使用映射类型会特别有用。

// 用户的配置值
type AppConfig = {
  username: string;
  layout: string;
};
// 用户是否有权更改配置值
type AppPermissions = {
  changeUsername: boolean;
  changeLayout: boolean;
};

在上面的代码中,AppConfig 和 AppPermissions 之间是存在隐式关系的,每当向 AppConfig 添加新的配置值时,AppPermissions 中也必须有相应的布尔值。

这里可以使用映射类型来管理两者之间的关系:

type AppConfig = {
  username: string;
  layout: string;
};
type AppPermissions = {
  [Property in keyof AppConfig as `change${Capitalize<Property>}`]: boolean
};

在上面的代码中,只要 AppConfig 中的类型发生变化,AppPermissions 就会随之变化。实现了两者之间的映射关系。

(2)概念

在 TypeScript 和 JavaScript 中,最常见的映射就是 Array.prototype.map():

typescript

复制代码

[1, 2, 3].map(value => value.toString()); // ["1", "2", "3"]

这里,我们将数组中的数字映射到其字符串的表示形式。因此,TypeScript 中的映射类型意味着将一种类型转换为另一种类型,方法就是对其每个属性进行转换。

(3)实例

下面来通过一个例子来深入理解以下映射类型。对设备定义以下类型,其包含制造商和价格属性:

type Device = {
  manufacturer: string;
  price: number;
};

为了让用户更容易理解设备信息,因此为对象添加一个新类型,该对象可以使用适当的格式来格式化设备的每个属性:

type DeviceFormatter = {
  [Key in keyof Device as `format${Capitalize<Key>}`]: (value: Device[Key]) => string;
};

我们来拆解一下上面的代码。Key in keyof Device 使用 keyof 类型运算符生成 Device 中所有键的并集。将它放在索引签名中实际上是遍历 Device 的所有属性并将它们映射到 DeviceFormatter 的属性。

format${Capitalize<Key>} 是映射的转换部分,它使用 key 重映射和模板文字类型将属性名称从 x 更改为 formatX。

(value: Device[Key]) => string; 利用索引访问类型 Device[Key] 来指示格式化函数的 value 参数是格式化的属性的类型。因此,formatManufacturer 接受一个 string(制造商),而 formatPrice 接受一个number(价格)。

下面是 DeviceFormatter 类型的样子:

type DeviceFormatter = {
  formatManufacturer: (value: string) => string;
  formatPrice: (value: number) => string;
};

现在,假设将第三个属性 releaseYear 添加到 Device 类型中:

type Device = {
  manufacturer: string;
  price: number;
  releaseYear: number;
}

由于映射类型的强大功能,DeviceFormatter 类型会自动扩展为如下类型,无需进行任何额外的工作:

type DeviceFormatter = {
  formatManufacturer: (value: string) => string;
  formatPrice: (value: number) => string;
  formatReleaseYear: (value: number) => string;
};


3. 实用程序中的映射


TypeScript 附带了许多用作实用程序的映射类型,最常见的包括 Omit、Partial、Readonly、Readonly、Exclude、Extract、NonNullable、ReturnType 等。下面来看看其中的两个是如何构建的。

(1)Partial

Partial 是一种映射类型,可以将已有的类型属性转换为可选类型,并通过使用与 undefined 的联合使类型可以为空。

interface Point3D {
    x: number;
    y: number;
    z: number;
}
type PartialPoint3D = Partial<Point3D>;

这里的 PartialPoint3D 类型实际是这样的:

typescript

复制代码

typePartialPoint3D = {
    x?: number;    y?: number;    z?: number;}

当我们鼠标悬浮在 Partial 上时,就会看到它的定义:44.webp.jpg把它拿出来:

typescrip

typePartial<T> = { [P in keyof T]?: T[P] | undefined; }

下面来拆解一下这行代码:

  • 使用泛型来传递目标接口 T;
  • 使用 keyof T 来获取 T 的所有 key。
  • 通过使用 [P in keyof T] 来访问并循环所有的 key;
  • 它通过添加 ? 使 key 成为可选的。
  • 使用联合类型 T[P] | undefined 使 key 的类型可以为空;

(2)Exclude

Exclude 是一种映射类型,可让有选择地从类型中删除属性。 其定义如下:

typeExclude<T, U> = T extends U ? never : T

它通过使用条件类型从 T 中排除那些可分配给 U 的类型,并且在排除的属性上返回 nerver。

type animals = 'bird' | 'cat' | 'crocodile';

type mamals = Exclude<animals, 'crocodile'>;  // 'bird' | 'cat'


4. 构建映射类型


通过上面的对 TypeScript 内置实用程序类型的原理解释,对映射类型有了更深的理解。最后,我们来构建一个自己的映射类型:Optional,它可以将原类型中指定 key 的类型置为可选的并且可以为空。

我们可以这样做:

  • 将整个类型转换为 Optional
  • 从该新类型中仅选择想要的属性使其成为可选的。
  • 将原始类型与排除的属性连接起来。

实现代码及测试用例如下:

type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
type Person = {
  name: string;
  surname: string;
  email: string;
}
type User = Optional<Person, 'email'>;
// 现在 email 属性是可选的
type AnonymousUser = Optional<Person, 'name' | 'surname'>;
// 现在 email 和 surname 属性是可选的

注意,这里使用 K extends keyof T 来确保只能传递属于类型/接口的属性。 否则,TypeScript 将在编译时抛出错误。

映射类型的一大优点就是它们的可组合性:可以组合它们来创建新的映射类型。

上面使用了已有的实用程序类型实现了我们想要的 Optional。当然,我们也可以在不使用任何其他映射类型的情况下重新创建 Optional 映射类型实用程序:


type Optional<T, K extends keyof T> =
    { [P in K]?: T[P] }
    &
    { [P in Exclude<keyof T, K>]: T[P] };

上面的代码结合了两种类型:

  • 第一种类型通过使用 ? 修饰符使 T 的所有 K 的 key 都是可选的。
  • 第二种类型通过使用 Excluse<keyof T,K>来获取剩余的key。
相关文章
|
21天前
|
JavaScript 编译器
TypeScript中类型守卫:缩小类型范围的艺术
【4月更文挑战第23天】TypeScript中的类型守卫是缩小类型范围的关键技术,它帮助我们在运行时确保值的精确类型,提升代码健壮性和可读性。类型守卫包括`typeof`(检查原始类型)、`instanceof`(检查类实例)和自定义类型守卫。通过这些方法,我们可以更好地处理联合类型、泛型和不同数据源,降低运行时错误,提高代码质量。
|
14天前
|
JavaScript 安全 前端开发
【TypeScript技术专栏】TypeScript中的类型推断与类型守卫
【4月更文挑战第30天】TypeScript的类型推断与类型守卫是提升代码安全的关键。类型推断自动识别变量类型,减少错误,包括基础、上下文、最佳通用和控制流类型推断。类型守卫则通过`typeof`、`instanceof`及自定义函数在运行时确认变量类型,确保类型安全。两者结合使用,优化开发体验,助力构建健壮应用。
|
14天前
|
JavaScript 前端开发 开发者
【TypeScript技术专栏】TypeScript类型系统与接口详解
【4月更文挑战第30天】TypeScript扩展JavaScript,引入静态类型检查以减少错误。其类型系统包括基本类型、数组等,而接口是定义对象结构的机制。接口描述对象外形,不涉及实现,可用于规定对象属性和方法。通过声明、实现接口,以及利用可选、只读属性,接口继承和合并,TypeScript增强了代码的健壮性和维护性。学习和掌握TypeScript的接口对于大型项目开发至关重要。
|
14天前
|
JavaScript 安全 前端开发
【亮剑】TypeScript 由于其强类型的特性,直接为对象动态添加属性可能会遇到一些问题
【4月更文挑战第30天】本文探讨了在 TypeScript 中安全地为对象动态添加属性的方法。基础方法是使用索引签名,允许接受任何属性名但牺牲了部分类型检查。进阶方法是接口扩展,通过声明合并动态添加属性,保持类型安全但可能导致代码重复。高级方法利用 OOP 模式的类继承,确保类型安全但增加代码复杂性。选择哪种方法取决于应用场景、代码复杂性和类型安全性需求。
|
15天前
|
JavaScript 前端开发
TypeScript基础类型
TypeScript基础类型
|
15天前
|
JavaScript 前端开发
typescript 混合类型
typescript 混合类型
|
20天前
|
JavaScript 前端开发 开发者
类型检查:结合TypeScript和Vue进行开发
【4月更文挑战第24天】TypeScript是JavaScript超集,提供类型注解等特性,提升代码质量和可维护性。Vue.js是一款高效前端框架,两者结合优化开发体验。本文指导如何配置和使用TypeScript与Vue:安装TypeScript和Vue CLI,创建Vue项目时选择TypeScript支持,配置`tsconfig.json`,编写`.tsx`组件,最后运行和构建项目。这种结合有助于错误检查和提升开发效率。
|
21天前
|
JavaScript 编译器 开发者
TypeScript中的类型推断机制:原理与实践
【4月更文挑战第23天】TypeScript的类型推断简化编码,提高代码可读性。编译器基于变量初始值或上下文推断类型,若新值不兼容则报错。文章深入探讨了类型推断原理和实践,包括基本类型、数组、函数参数与返回值、对象类型的推断,并提醒注意类型推断的限制,如非万能、类型兼容性和适度显式指定类型。了解这些能帮助更好地使用TypeScript。
|
21天前
|
JavaScript 前端开发 编译器
TypeScript中的高级类型:联合类型、交叉类型与条件类型深入解析
【4月更文挑战第23天】探索TypeScript的高级类型。这些特性增强类型系统的灵活性,提升代码质量和维护性。
|
21天前
|
JavaScript 安全 编译器
TypeScript中类型断言的使用与风险
【4月更文挑战第23天】TypeScript中的类型断言用于显式指定值的类型,但在不恰当使用时可能引发运行时错误或降低代码可读性。