搞懂 TypeScript 中的對映型別(Mapped Types)

pingan8787發表於2022-04-17

本文會和大家詳細介紹 TypeScript 中的對映型別(Mapped Type),看完本文你將學到以下知識點:

  • 數學中的對映和 TS 中的對映型別的關係;
  • TS 中對映型別的應用;
  • TS 中對映型別修飾符的應用;

接下來會先從「數學中的對映」開始介紹。

本文使用到的 TypeScript 版本為 v4.6.2

如果你對 TypeScript 還不熟悉,可以看下面幾篇資料:

  1. 一份不可多得的 TS 學習指南(1.8W字)
  2. 了不起的 TypeScript 入門教程

一、什麼是對映?

在學習 TypeScript 型別系統時,儘量多和數學中的集合類比學習,比如 TypeScript 中的聯合型別,類似數學中的並集等。

在數學中,對映是指兩個元素的集合之間元素相互對應的關係,比如下圖:

image.png
(來源:https://baike.baidu.com/item/%E6%98%A0%E5%B0%84/20402621

可以將對映理解為函式,如上圖,當我們需要將集合 A 的元素轉換為集合 B 的元素,可以通過 f函式做對映,比如將集合 A 的元素 1對應到集合 B 中的元素 2
這樣就能很好的實現對映過程的複用

二、TypeScript 中的對映型別是什麼?

1. 概念介紹

TypeScript 中的對映型別和數學中的對映類似,能夠將一個集合的元素轉換為新集合的元素,只是 TypeScript 對映型別是將一個型別對映成另一個型別

在我們實際開發中,經常會需要一個型別的所有屬性轉換為可選型別,這時候你可以直接使用 TypeScript 中的 Partial工具型別:

type User = {
  name: string;
  location: string;
  age: number;
}

type User2 = Partial<User>;
/*
  User2 的型別:
  
  type User2 = {
      name?: string | undefined;
      location?: string | undefined;
      age?: number | undefined;
  }
*/

這樣我們就實現了將 User型別對映成 User2型別,並且將 User型別中的所有屬性轉為可選型別。

image.png

2. 實現方法

TypeScript 對映型別的語法如下:

type TypeName<Type> = {
  [Property in keyof Type]: boolean;
};

我們既然可以通過 Partial工具型別非常簡單的實現將指定型別的所有屬性轉換為可選型別,那其內容原理又是如何?

我們可以在編輯器中,將滑鼠懸停在 Partial名稱上面,可以看到編輯器提示如下:

image.png

拆解一下其中每個部分:

  • type Partial<T>:定義一個型別別名 Partial和泛型 T
  • keyof T:通過 keyof操作符獲取泛型 T中所有 key,返回一個聯合型別(如果不清楚什麼是聯合型別,可以理解為一個陣列);
type User = {
  name: string;
  location: string;
  age: number;
}

type KeyOfUser = keyof User; // "name" | "location" | "age"
  • in:類似 JS 中 for...in中的 in,用來遍歷目標型別的公開屬性名;
  • T[P]:是個索引訪問型別(也稱查詢型別),獲取泛型 TP型別,類似 JS 中的訪問物件的方式;
  • ?:將型別值設定為可選型別;
  • { [P in keyof T] ?: T[P] | undefined}:遍歷 keyof T返回的聯合型別,並定義用 P變數接收,其每次遍歷返回的值為可選型別的 T[P]

這樣就實現了 Partial工具型別,這種操作方法非常重要,是後面進行 TypeScript 型別體操的重要基礎。

關於型別體操的練習,有興趣可以看看這篇文章:
《這 30 道 TS 練習題,你能答對幾道?》https://juejin.cn/post/7009046640308781063

三、對映型別的應用

TypeScript 對映型別經常用來複用一些對型別的操作過程,比如 TypeScript 目前支援的 21 種工具型別,將我們常用的一些型別操作定義成這些工具型別,方便開發者複用這些型別。

所有已支援的工具型別可以看下官方文件:
https://www.typescriptlang.org/docs/handbook/utility-types.html

下面我們挑幾個常用的工具型別,看下其實現過程中是如何使用對映型別的。

在學習 TypeScript 過程中,推薦多在官方的 Playground 練習和學習:
https://www.typescriptlang.org/zh/play

1. Required 必選屬性

用來將型別的所有屬性設定為必選屬性

實現如下:

type Required<T> = {
    [P in keyof T]-?: T[P];
};

使用方式:

type User = {
  name?: string;
  location?: string;
  age?: number;
}

type User2 = Required<User>;
/*
  type User2 = {
      name: string;
      location: string;
      age: number;
  }
*/

const user: User2 = {
  name: 'pingan8787',
  age: 18
}
/*
  報錯:
  Property 'location' is missing in type '{ name: string; age: number; }'
  but required in type 'Required<User>'.
*/

這邊的 -?符號可以暫時理解為“將可選屬性轉換為必選屬性”,下一節會詳細介紹這些符號。

2. Readonly 只讀屬性

用來將所有屬性的型別設定為只讀型別,即不能重新分配型別。

實現如下:

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
}

使用方式:

type User = {
  name?: string;
  location?: string;
  age?: number;
}

type User2 = Readonly<User>;
/*
  type User2 = {
      readonly name?: string | undefined;
      readonly location?: string | undefined;
      readonly age?: number | undefined;
  }
*/

const user: User2 = {
  name: 'pingan8787',
  age: 18
}

user.age = 20;
/*
  報錯:
  Cannot assign to 'age' because it is a read-only property.
*/

3. Pick 選擇指定屬性

用來從指定型別中選擇指定屬性並返回

實現如下:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
}

使用如下:

type User = {
  name?: string;
  location?: string;
  age?: number;
}

type User2 = Pick<User, 'name' | 'age'>;
/*
  type User2 = {
      name?: string | undefined;
      age?: number | undefined;
  }
*/

const user1: User2 = {
  name: 'pingan8787',
  age: 18
}

const user2: User2 = {
  name: 'pingan8787',
  location: 'xiamen', // 報錯
  age: 18
}
/*
  報錯
  Type '{ name: string; location: string; age: number; }' is not assignable to type 'User2'.
  Object literal may only specify known properties, and 'location' does not exist in type 'User2'.
*/

4. Omit 忽略指定屬性

作用類似與 Pick工具型別相反,可以從指定型別中忽略指定的屬性並返回。

實現如下:

type Omit<T, K extends string | number | symbol> = {
  [P in Exclude<keyof T, K>]: T[P];
}

使用方式:

type User = {
  name?: string;
  location?: string;
  age?: number;
}

type User2 = Omit<User, 'name' | 'age'>;
/*
  type User2 = {
      location?: string | undefined;
  }
*/

const user1: User2 = {
  location: 'xiamen',
}

const user2: User2 = {
  name: 'pingan8787', // 報錯
  location: 'xiamen'
}
/*
  報錯:
  Type '{ name: string; location: string; }' is not assignable to type 'User2'.
  Object literal may only specify known properties, and 'name' does not exist in type 'User2'.
*/

5. Exclude 從聯合型別中排除指定型別

用來從指定的聯合型別中排除指定型別

實現如下:

type Exclude<T, U> = T extends U ? never : T;

使用方式:

type User = {
  name?: string;
  location?: string;
  age?: number;
}

type User2 = Exclude<keyof User, 'name'>;
/*
  type User2 = "location" | "age"
*/

const user1: User2 = 'age';
const user2: User2 = 'location';
const user3: User2 = 'name';  // 報錯
/*
  報錯:
  Type '"name"' is not assignable to type 'User2'.
*/

四、對映修飾符的應用

在自定義對映型別的時候,我們可以使用兩個對映型別的修飾符來實現我們的需求:

  • readonly修飾符:將指定屬性設定為只讀型別
  • ?修飾符:將指定屬性設定為可選型別

前面介紹 ReadonlyPartial工具型別的時候已經使用到:

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
}

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

當然,也可以對修飾符進行操作:

  • +新增修飾符(預設使用);
  • -刪除修飾符;

比如:

type Required<T> = {
    [P in keyof T]-?: T[P]; // 通過 - 刪除 ? 修飾符
};

也可以放在前面使用:

type NoReadonly<T> = {
  -readonly [P in keyof T]: T[P]; // 通過 - 刪除 readonly 修飾符
}

五、總結

本文從數學中的對映作為切入點,詳細介紹 TypeScript 對映型別(Mapped Type)並介紹對映型別的應用和修飾符的應用。

在學習 TypeScript 型別系統時,儘量多和數學中的集合類比學習,比如 TypeScript 中的聯合型別,類似數學中的並集等。

學好對映型別,是接下來做型別體操中非常重要的基礎~~

參考資料

  1. TypeScript 文件-對映型別:https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
  2. TypeScript 工具型別: https://www.typescriptlang.org/docs/handbook/utility-types.html

相關文章