天天看點

用了 TS 映射類型,同僚直呼内行!

在日常工作中,使用者注冊是一個很常見的場景。這裡我們可以使用 TS 定義一個 User 類型,在該類型中所有的鍵都是必填的。

type User = {
  name: string; // 姓名
  password: string; // 密碼
  address: string; // 位址
  phone: string; // 聯系電話
};      

通常情況下,對于已注冊的使用者,我們是允許使用者隻修改部分使用者資訊。這時我們就可以定義一個新的 UserPartial 類型,表示用于更新的使用者對象的類型,在該類型中所有的鍵都是可選的。

type UserPartial = {
  name?: string; // 姓名
  password?: string; // 密碼
  address?: string; // 位址
  phone?: string; // 聯系電話
};      
用了 TS 映射類型,同僚直呼内行!

而對于檢視使用者資訊的場景,我們希望該使用者對象所對應的對象類型中所有的鍵都是隻讀。針對這種需求,我們可以定義 ReadonlyUser 類型。

type ReadonlyUser = {
  readonly name: string; // 姓名
  readonly password: string; // 密碼
  readonly address: string; // 位址
  readonly phone: string; // 聯系電話
};      
用了 TS 映射類型,同僚直呼内行!

回顧前面已定義的與使用者相關的 3 種類型,你會發現它們中含有很多重複的代碼。

用了 TS 映射類型,同僚直呼内行!

那麼如何減少以上類型中的重複代碼呢?答案是可以使用映射類型,它是一種泛型類型,可用于把原有的對象類型映射成新的對象類型。

用了 TS 映射類型,同僚直呼内行!
用了 TS 映射類型,同僚直呼内行!

映射類型的文法如下:

{ [ P in K ] : T }      

其中 P in K 類似于 JavaScript 中的 ​

​for...in​

​ 語句,用于周遊 K 類型中的所有類型,而 T 類型變量用于表示 TS 中的任意類型。

在映射的過程中,你還可以使用 ​

​readonly​

​ 和 ? 這兩個額外的修飾符。通過添加 + 和 - 字首,來增加和移除對應的修飾符。如果沒有添加任何字首的話,預設是使用 +。

用了 TS 映射類型,同僚直呼内行!

現在我們就可以總結出常見的映射類型文法:

{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }      

介紹完映射類型的文法,我們來看一些具體的例子:

type Item = { a: string; b: number; c: boolean };

type T1 = { [P in "x" | "y"]: number }; // { x: number, y: number }
type T2 = { [P in "x" | "y"]: P }; // { x: "x", y: "y" }
type T3 = { [P in "a" | "b"]: Item[P] }; // { a: string, b: number }
type T4 = { [P in keyof Item]: Item[P] }; // { a: string, b: number, c: boolean }      

下面我們來看一下如何利用映射類型來重新定義 UserPartial 類型:

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

type UserPartial = MyPartial<User>;      

在以上代碼中,我們定義了 MyPartial 映射類型,然後利用該類型把 User 類型映射成 UserPartial 類型。

用了 TS 映射類型,同僚直呼内行!

其中 keyof 操作符用于擷取某種類型中的所有鍵,其傳回類型是聯合類型。而類型變量 P 會随着每次周遊改變成不同的類型,​

​T[P]​

​ 該文法類似于屬性通路的文法,用于擷取對象類型某個屬性對應值的類型。

TypeScript 4.1 版本允許我們使用 as 子句對映射類型中的鍵進行重新映射。它的文法如下:

type MappedTypeWithNewKeys<T> = {
    [K in keyof T as NewKeyType]: T[K]
    //            ^^^^^^^^^^^^^
    //            這是新的文法!
}      
用了 TS 映射類型,同僚直呼内行!

其中 NewKeyType 的類型必須是 string | number | symbol 聯合類型的子類型。使用 as 子句,我們可以定義一個 Getters 工具類型,用于為對象類型生成對應的 Getter 類型:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () T[K]
};

interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters<Person>;
// {
//   getName: () => string; 
//   getAge: () => number;
//   getLocation: () => string;
// }      

在以上代碼中,因為 keyof  T 傳回的類型可能會包含 symbol 類型,而 Capitalize 工具類型要求處理的類型需要是 string 類型的子類型,是以需要通過交叉運算符進行類型過濾。

此外,在對鍵進行重新映射的過程中,我們可以通過傳回 never 類型對鍵進行過濾:

// Remove the 'kind' property
type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, "kind">]: T[K]
};

interface Circle {
    kind: "circle";
    radius: number;
}

type KindlessCircle = RemoveKindField<Circle>;
//   type KindlessCircle = {
//       radius: number;
//   };      

看完本文之後,相信你已經了解映射類型的作用了,也知道 TS 内部一些工具類型是如何實作的。你喜歡以這種形式學 TS 麼?