去年12月的 TypeScript 2.1 中加入了 keyof / Lookup Types / Mapped Types 等 (編譯期的) 型別運算特性。
本文將介紹這些特性,並用這些特性實現一個 “遞迴的Readonly” 泛型。
新特性的介紹
keyof
keyof T
返回一個型別,這個型別是一個 string literal 的 union,內容是T中所有的屬性名 (key)。
例: keyof { a: 1, b: 2 }
得到的型別是 "a" | "b"
Lookup Types / 查詢型別
[]
的型別版。
T[K]
返回 (型別T中以K為屬性名的值) 的型別。K
必須是 keyof T
的子集,可以是一個字串字面量。
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的返回值 (即對映全部屬性)。
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
等型別。
Readonly的強化版: DeepReadonly
前面提到的 Readonly
只限制屬性只讀,不會把屬性的屬性也變成只讀:
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>
時有如下的附加規則:
-
基本型別 (
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有時會導致一些不直觀的結果,不過大多數情況下我們不是想要基本型別的同構型別,到此停止展開可以接受)