1. 程式人生 > 程式設計 >詳解TypeScript對映型別和更好的字面量型別推斷

詳解TypeScript對映型別和更好的字面量型別推斷

概述

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

使用對映型別,可以捕獲型別系統中類似Object.freeze()等方法的效果。凍結物件後,就不能再新增、更改或刪除其中的屬性。來看看如何在不使用對映型別的情況下在型別系統中對其進行編碼:

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;

咱們定義了一個包含x和y兩個屬性的Point介面,咱們還定義了另一個介面FrozenPoint,它與Point相同,只是它的所有屬性都被使用readonly定義為只讀屬性。

freezePoint函式接受一個Point作為引數並凍結該引數,接著,向呼叫者返回相同的物件。然而,該物件的型別已更改為FrozenPoint,因此其屬性被靜態型別化為只讀。這就是為什麼當試圖將42賦值給x屬性時,TypeScript會出錯。在執行時,分配要麼丟擲一個型別錯誤(嚴格模式),要麼靜默失敗(非嚴格模式)。

雖然上面的示例可以正確地編譯和工作,但它有兩大缺點

  • 需要兩個介面。除了Point型別之外,還必須定義FrozenPoint型別,這樣才能將readonly修飾符新增到兩個屬性中。當咱們更改Point時,還必須更改FrozenPoint,這很容易出錯,也很煩人。
  • 需要 freezePoint函式。對於希望在應用程式中凍結的每種型別的物件,咱們就必須定義一個包裝器函式,該函式接受該型別的物件並返回凍結型別的物件。沒有對映型別,咱們就不能以通用的方式靜態地使用Object.freeze()。

使用對映型別構建 Object.freeze()

來看看Object.freeze()是如何在lib.d.ts檔案中定義的:

/**
  * 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 Readhttp://www.cppcns.comonly<T> = {
  readonly [P in keyof T]: T[P]
};

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

  • 用一個名為T的型別引數定義了一個泛型 Readonly。
  • 在方括號中,使用了keyof操作符。keyof T將T型別的所有屬性名錶示為字串字面量型別的聯合。
  • 方括號中的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]
};

現在咱們知道T是Point,可以確定keyof Point表示的字串字面量型別的並集:

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

型別P表示每個屬性x和y,咱們把它們作為單獨的屬性來寫,去掉對映的型別語法

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

最後,咱們可以解析這兩種查詢型別,並將它們替換為具體的x和y型別,這兩種型別都是number。

type Re程式設計客棧adonlyPoint = {
  readonly x: number;
  readonly y: number;
};

最後,得到的ReadonlyPoint型別與咱們手動建立的FrozenPoint型別相同。

更多對映型別的示例

上面已經看到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 o程式設計客棧f 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>的一個很好的用例。
  • Lodash:pick函式從一個物件中選擇一組屬性。該方法返回一個新物件,該物件只包含咱們選擇的屬性。可以使用Pick<T>對該行為進行構建,正如其名稱所示。

更好的字面量型別推斷

字串、數字和布林字面量型別(如:"abc",1和true)之前僅在存在顯式型別註釋時才被推斷。從 TypeScript 2.1 開始,字面量型別總是推斷為預設值。在 TypeScript 2.0 中,型別系統擴充套件了幾個新的字面量型別:

  • boolean字面量型別
  • 數字字面量
  • 列舉字面量

不帶型別註解的const變數或readonly屬性的型別推斷為字面量初始化的型別。已經初始化且不帶型別註解的let變數、var變數、形參或非readonly屬性的型別推斷為初始值的擴充套件字面量型別。字串字面量擴充套件型別是string,數字字面量擴充套件型別是number,true或false的字面量型別是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 rhttp://www.cppcns.comememberMe = true;
// 推斷型別: true

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

enum FlexDirection {
  Row,Column
}

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

注意,direction型別為FlexDirection.Column,它http://www.cppcns.com是列舉字面量型別。如果使用let或var關鍵字來宣告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”時,一切就都解決了。

以上就是詳解TypeScript對映型別和更好的字面量型別推斷的詳細內容,更多關於TS的資料請關注我們其它相關文章!