【TypeScript 演化史 -- 7】映射類型和更好的字面量類型推斷

做者:Marius Schulz
譯者:前端小智
來源:Marius Schulz

乾貨系列文章彙總以下,以爲不錯點個Star:前端

Github: https://github.com/qq44924588...](https://github.com/qq44924588...git


爲了保證的可讀性,本文采用意譯而非直譯。github

TypeScript 2.1 引入了映射類型,這是對類型系統的一個強大的補充。本質上,映射類型容許w我們經過映射屬性類型從現有類型建立新類型。根據我們指定的規則轉換現有類型的每一個屬性。轉換後的屬性組成新的類型。算法

使用映射類型,能夠捕獲類型系統中相似 Object.freeze() 等方法的效果。凍結對象後,就不能再添加、更改或刪除其中的屬性。來看看如何在不使用映射類型的狀況下在類型系統中對其進行編碼:typescript

interface Point {
  x: number;
  y: number;
}

interface FrozenPoint {
  readonly x: number;
  readonly y: number;
}

function freezePoint(p: Point): FrozenPoint {
  return Object.freeze(p);
}

const origin = freezePoint({ x: 0, y: 0 });

// Error! Cannot assign to 'x' because it
// is a constant or a read-only property.
origin.x = 42;

我們定義了一個包含 xy 兩個屬性的 Point 接口,我們還定義了另外一個接口FrozenPoint,它與 Point 相同,只是它的全部屬性都被使用 readonly 定義爲只讀屬性。segmentfault

freezePoint 函數接受一個 Point 做爲參數並凍結該參數,接着,向調用者返回相同的對象。然而,該對象的類型已更改成FrozenPoint,所以其屬性被靜態類型化爲只讀。這就是爲何當試圖將 42 賦值給 x 屬性時,TypeScript 會出錯。在運行時,分配要麼拋出一個類型錯誤(嚴格模式),要麼靜默失敗(非嚴格模式)。 api

雖然上面的示例能夠正確地編譯和工做,但它有兩大缺點微信

  1. 須要兩個接口。除了 Point 類型以外,還必須定義 FrozenPoint 類型,這樣才能將 readonly 修飾符添加到兩個屬性中。當我們更改 Point 時,還必須更改FrozenPoint,這很容易出錯,也很煩人。
  2. 須要 freezePoint 函數。對於但願在應用程序中凍結的每種類型的對象,我們就必須定義一個包裝器函數,該函數接受該類型的對象並返回凍結類型的對象。沒有映射類型,我們就不能以通用的方式靜態地使用 Object.freeze()

使用映射類型構建 Object.freeze()

來看看 Object.freeze()是如何在 lib.d.ts 文件中定義的:app

/**
  * Prevents the modification of existing property attributes and values, and prevents the addition of new properties.
  * @param o Object on which to lock the attributes.
  */
freeze<T>(o: T): Readonly<T>;

該方法的返回類型爲Readonly<T>,這是一個映射類型,它的定義以下:函數

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

這個語法一開始可能會讓人望而生畏,我們來一步一步分析它:

  • 用一個名爲 T 的類型參數定義了一個泛型 Readonly。
  • 在方括號中,使用了 keyof 操做符。keyof TT 類型的全部屬性名錶示爲字符串字面量類型的聯合。
  • 方括號中的 in 關鍵字表示咱們正在處理映射類型。[P in keyof T]: T[P]表示將 T類型的每一個屬性 P 的類型轉換爲 T[P]。若是沒有readonly修飾符,這將是一個身份轉換。
  • 類型 T[P] 是一個查找類型,它表示類型 T 的屬性 P 的類型。
  • 最後,readonly 修飾符指定每一個屬性都應該轉換爲只讀屬性。

由於 Readonly<T> 類型是泛型的,因此我們爲T提供的每種類型都正確地入了Object.freeze() 中。

const origin = Object.freeze({ x: 0, y: 0 });

// Error! Cannot assign to 'x' because it
// is a constant or a read-only property.
origin.x = 42;

映射類型的語法更直觀解釋

此次我們使用 Point 類型爲例來粗略解釋類型映射如何工做。請注意,如下只是出於解釋目的,並不能準確反映TypeScript使用的解析算法。

從類型別名開始:

type ReadonlyPoint = Readonly<Point>;

如今,我們能夠在 Readonly<T> 中爲泛型類型 T 的替換 Point 類型:

type ReadonyPoint = {
  readonly [P in keyof Point]: Point[P]
};

如今我們知道 TPoint,能夠肯定keyof Point表示的字符串字面量類型的並集:

type ReadonlyPoint = {
  readonly [P in "x" | "y"]: Point[p]
};

類型 P 表示每一個屬性 xy,我們把它們做爲單獨的屬性來寫,去掉映射的類型語法

type ReadonlyPoint = {
  readonly x: Point["x"];
  readonly y: Point["y"];
};

最後,我們能夠解析這兩種查找類型,並將它們替換爲具體的 xy 類型,這兩種類型都是 number

type ReadonlyPoint = {
  readonly x: number;
  readonly y: number;
};

最後,獲得的 ReadonlyPoint 類型與我們手動建立的 FrozenPoint 類型相同。



clipboard.png


更多映射類型的示例

上面已經看到 lib.d.ts 文件中內置的 Readonly <T> 類型。此外,TypeScript 定義了其餘映射類型,這些映射類型在各類狀況下都很是有用。以下:

/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P]
};

/**
 * From T pick a set of properties K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
};

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends string, T> = {
  [P in K]: T
};

這裏還有兩個關於映射類型的例子,若是須要的話,能夠本身編寫:

/**
 * Make all properties in T nullable
 */
type Nullable<T> = {
  [P in keyof T]: T[P] | null
};

/**
 * Turn all properties of T into strings
 */
type Stringify<T> = {
  [P in keyof T]: string
};

映射類型和聯合的組合也是頗有趣:

type X = Readonly<Nullable<Stringify<Point>>>;
// type X = {
//     readonly x: string | null;
//     readonly y: string | null;
// };

映射類型的實際用例

實戰中常常能夠看到映射類型,來看看 React 和 Lodash :

  • React:組件的 setState 方法容許我們更新整個狀態或其中的一個子集。我們能夠更新任意多個屬性,這使得setState方法成爲 Partial<T> 的一個很好的用例。
  • Lodashpick 函數從一個對象中選擇一組屬性。該方法返回一個新對象,該對象只包含我們選擇的屬性。可使Pick<T>該行爲進行構建,正如其名稱所示。

更好的字面量類型推斷

字符串、數字和布爾字面量類型(如:"abc"1true)以前僅在存在顯式類型註釋時才被推斷。從 TypeScript 2.1 開始,字面量類型老是推斷爲默認值。在 TypeScript 2.0 中,類型系統擴展了幾個新的字面量類型:

  • boolean 字面量類型
  • 數字字面量
  • 枚舉字面量

不帶類型註解的 const 變量或 readonly 屬性的類型推斷爲字面量初始化的類型。已經初始化且不帶類型註解的 let 變量、var 變量、形參或非 readonly 屬性的類型推斷爲初始值的擴展字面量類型。字符串字面量擴展類型是 string,數字字面量擴展類型是number,truefalse 的字面量類型是 boolean,還有枚舉字面量擴展類型是枚舉。

更好的 const 變量推斷

我們從局部變量和 var 關鍵字開始。當 TypeScript 看到下面的變量聲明時,它會推斷baseUrl變量的類型是 string

var baseUrl = "https://example.com/";
// 推斷類型: string

let 關鍵字聲明的變量也是如此

let baseUrl = "https://example.com/";
// 推斷類型: string

這兩個變量都推斷爲string類型,由於它們能夠隨時更改。它們是用一個字面量字符串值初始化的,可是之後能夠修改它們。

可是,若是使用const關鍵字聲明變量並使用字符串字面量進行初始化,則推斷的類型再也不是 string,而是字面量類型

const baseUrl = "https://example.com/";
// 推斷類型: "https://example.com/"

因爲常量字符串變量的值永遠不會改變,所以推斷出的類型會更加的具體。 baseUrl 變量沒法保存 "https://example.com/" 之外的任何其餘值。

字面量類型推斷也適用於其餘原始類型。若是用直接的數值或布爾值初始化常量,推斷出的仍是字面量類型

const HTTPS_PORT = 443;
// 推斷類型: 443

const rememberMe = true;
// 推斷類型: true

相似地,當初始化器是枚舉值時,推斷出的也是字面量類型:

enum FlexDirection {
  Row,
  Column
}

const direction = FlexDirection.Column;
// 推斷類型: FlexDirection.Column

注意,direction 類型爲 FlexDirection.Column,它是枚舉字面量類型。若是使用letvar 關鍵字來聲明 direction 變量,那麼它的推斷類型應該是 FlexDirection

更好的只讀屬性推斷

與局部 const 變量相似,帶有字面量初始化的只讀屬性也被推斷爲字面量類型

class ApiClient {
  private readonly baseUrl = "https://api.example.com/";
  // 推斷類型: "https://api.example.com/"

  get(endpoint: string) {
    // ...
  }
}

只讀類屬性只能當即初始化,也能夠在構造函數中初始化。試圖更改其餘位置的值會致使編譯時錯誤。所以,推斷只讀類屬性的字面量類型是合理的,由於它的值不會改變。

固然,TypeScript 不知道在運行時發生了什麼:用 readonly 標記的屬性能夠在任什麼時候候被一些JS 代碼改變。readonly 修飾符只限制從 TypeScript 代碼中對屬性的訪問,在運行時就無能爲力。也就是說,它會被編譯時刪除掉,不會出如今生成的 JS 代碼中。

推斷字面量類型的有用性

你可能會問本身,爲何推斷 const 變量和 readonly 屬性爲字面量類型是有用的。考慮下面的代碼:

const HTTP_GET = "GET"; // 推斷類型: "GET"
const HTTP_POST = "POST"; // 推斷類型: "POST"

function get(url: string, method: "GET" | "POST") {
  // ...
}

get("https://example.com/", HTTP_GET);

若是推斷 HTTP_GET 常量的類型是 string 而不是 「GET」,則會出現編譯時錯誤,由於沒法將HTTP_GET 做爲第二個參數傳遞給get函數:

Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'

固然,若是相應的參數只容許兩個特定的字符串值,則不容許將任意字符串做爲函數參數傳遞。可是,當爲兩個常量推斷字面量類型「GET」「POST」時,一切就都解決了。


編輯中可能存在的bug無法實時知道,過後爲了解決這些bug,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:
https://mariusschulz.com/blog...
https://mariusschulz.com/blog...


交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq449245884/xiaozhi

由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。

clipboard.png

相關文章
相關標籤/搜索