去年12月的 TypeScript 2.1 中加入了 keyof / Lookup Types / Mapped Types 等 (編譯期的) 類型運算特性。
本文將介紹這些特性,並用這些特性實現一個 "遞歸的Readonly" 泛型。git
keyof T
返回一個類型,這個類型是一個 string literal 的 union,內容是T中全部的屬性名 (key)。github
例: keyof { a: 1, b: 2 }
獲得的類型是 "a" | "b"
typescript
[]
的類型版。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 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]>; }
使用上面介紹的新特性能夠定義出一些可用做 類型的 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
只限制屬性只讀,不會把屬性的屬性也變成只讀: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;
(這個話題是 @vilicvane 幫我審稿時提到的。我又翻了一下 相關的 issue 後以爲滿有意思... 就一塊兒加進來了。不讀這個在大多數狀況下應該不影響使用。)
背景: 若是 A 是泛型的類型參數 (好比 T<A>
),則稱形如 { [P in keyof A]: (類型表達式) }
的映射類型爲 A 的 同構 (isomorphic) 類型。這樣的類型含有和 A 相同的屬性名,即相同的"形狀"。在展開 T<A>
時有以下的附加規則:
基本類型 (string | number | boolean | undefined | null
) 的同構類型強行定義爲其自己,即跳過了對值類型的運算
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有時會致使一些不直觀的結果,不過大多數狀況下咱們不是想要基本類型的同構類型,到此中止展開能夠接受)