TypeScript 2.1中的類型運算 & 一個遞歸的Readonly泛型

去年12月的 TypeScript 2.1 中加入了 keyof / Lookup Types / Mapped Types 等 (編譯期的) 類型運算特性。
本文將介紹這些特性,並用這些特性實現一個 "遞歸的Readonly" 泛型。git

新特性的介紹

keyof

keyof T 返回一個類型,這個類型是一個 string literal 的 union,內容是T中全部的屬性名 (key)。github

例: keyof { a: 1, b: 2 } 獲得的類型是 "a" | "b"typescript

Lookup Types / 查找類型

[] 的類型版。app

T[K] 返回 (類型T中以K爲屬性名的值) 的類型。K 必須是 keyof T 的子集,能夠是一個字符串字面量。code

const a = { k1: 1, k2: "v2" };

// tv1 爲number
type tv1 = (typeof a)["k1"];

// tv2 爲string
type tv2 = (typeof a)["k2"];

// tv$ 爲 (number|string): 屬性名的並集對應到了屬性值的類型的並集
type tv$ = (typeof a)["k1" | "k2"];

// 以上的括號不是必需的: typeof 優先級更高

// 也能夠用於獲取內置類型 (string 或 string[]) 上的方法的類型

// (pos: number) => string
type t_charAt = string["charAt"];  

// (...items: string[]) => number
type t_push = string[]["push"];

Mapped Types / 映射類型

咱們能夠在類型定義中引用其餘類型的 (部分或所有) 屬性,並對其進行運算,用運算結果定義出新的類型 (Mapped Type)。即"把舊類型的屬性 map (映射) 成新類型的屬性",能夠比做 list comprehension (把舊 list 的成員 map 成新 list 的成員) 的類型屬性版。遞歸

引用哪些屬性一樣是經過一個 string literal 的 union 來定義的。這個union必須是 keyof 舊類型 的子集,能夠是一個或多個 string literal,也能夠是keyof的返回值 (即映射所有屬性)。ip

interface A {
    k1: string;
    k2: string;
    k3: number;
}

// 從A中取一部分屬性,類型不變 (A[P] 是上面講的查找類型)
// 結果: type A_var1 = { k1: string, k3: number }
type A_var1 = {
    [P in "k1" | "k3"]: A[P];
}

// 從A中取全部屬性, 類型改成number
// 結果: type A_var1 = { k1: number, k2: number, k3: number }
// **注意** keyof / Mapped type / 泛型一塊兒使用時有一些特殊規則。建議讀一下最後一部分 "DeepReadonly 是怎樣展開的"
type A_var2 = {
    [P in keyof A]: number;
}

// 從A中取全部屬性, 類型改成相應的Promise (TS 2.1 release note中的Deferred是這個的泛型版)
type A_var3 = {
    [P in keyof A]: Promise<A[P]>;
}

新特性的例子: Readonly

使用上面介紹的新特性能夠定義出一些可用做 類型的 decorator 的泛型,好比下面的 Readonly (已經在TS2.1標準庫中):字符串

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

interface A {
    k1: string;
    k2: string;
    k3: number;
}

/**
 類型運算的結果爲
type A_ro = {
    readonly k1: string;
    readonly k2: string;
    readonly k3: number;
}
 */
type A_ro = Readonly<A>;

利用這些類型運算,咱們能夠表達出更復雜的編譯期約束,十分適合 (須要和無限的類型一塊兒工做的) 的代碼或庫。好比 Release note 中還提到的Partial / Pick / Record 等類型。get

Readonly的強化版: DeepReadonly

前面提到的 Readonly 只限制屬性只讀,不會把屬性的屬性也變成只讀:string

const v = { k1: 1, k2: { k21: 2 } };

const v_ro = v as Readonly<typeof v>;

// 屬性: 不可賦值
v_ro.k1 = 2; 
// 屬性的屬性: 能夠賦值
v_ro.k2.k21 = 3;

咱們能夠寫一個DeepReadonly,實現遞歸的只讀:

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

const v_deep_ro = v as any as DeepReadonly<typeof v>;
// 屬性: 不可賦值
v_deep_ro.k1 = 2;
// 屬性的屬性: 也不可賦值
v_deep_ro.k2.k21 = 3;

DeepReadonly 是怎樣展開的

(這個話題是 @vilicvane 幫我審稿時提到的。我又翻了一下 相關的 issue 後以爲滿有意思... 就一塊兒加進來了。不讀這個在大多數狀況下應該不影響使用。)

背景: 若是 A 是泛型的類型參數 (好比 T<A>),則稱形如 { [P in keyof A]: (類型表達式) } 的映射類型爲 A 的 同構 (isomorphic) 類型。這樣的類型含有和 A 相同的屬性名,即相同的"形狀"。在展開 T<A> 時有以下的附加規則:

  1. 基本類型 (string | number | boolean | undefined | null) 的同構類型強行定義爲其自己,即跳過了對值類型的運算

  2. union 類型 (如 type A = A1 | A2) 的同構類型 T<A> 展開爲 T<A1> | T<A2>

因此上面的 DeepReadonly<typeof v>的 (概念上) 展開過程是這樣的 :

type T_DeepRO = DeepReadonly<{ k1: number; k2: { k21: number } }>

type T_DeepRO = {
    readonly k1: number;
    readonly k2: DeepReadonly<{ k21: number }>;
}

type T_DeepRO = {
    readonly k1: number;
    readonly k2: {
        readonly k21: DeepReadonly<number>;
    }
}

↓ (規則1)

type T_DeepRO = {
    readonly k1: number;
    readonly k2: {
        readonly k21: number;
    }
}

(規則1有時會致使一些不直觀的結果,不過大多數狀況下咱們不是想要基本類型的同構類型,到此中止展開能夠接受)

相關文章
相關標籤/搜索