標籤:

TypeScript 2.1中的類型運算

去年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" };nn// tv1 為numberntype tv1 = (typeof a)["k1"];nn// tv2 為stringntype tv2 = (typeof a)["k2"];nn// tv$ 為 (number|string): 屬性名的並集對應到了屬性值的類型的並集ntype tv$ = (typeof a)["k1" | "k2"];nn// 以上的括弧不是必需的: typeof 優先順序更高nn// 也可以用於獲取內置類型 (string 或 string[]) 上的方法的類型nn// (pos: number) => stringntype t_charAt = string["charAt"]; nn// (...items: string[]) => numberntype t_push = string[]["push"];n

Mapped Types / 映射類型

我們可以在類型定義中引用其他類型的 (部分或全部) 屬性,並對其進行運算,用運算結果定義出新的類型 (Mapped Type)。即」把舊類型的屬性 map (映射) 成新類型的屬性」,可以比作 list comprehension (把舊 list 的成員 map 成新 list 的成員) 的類型屬性版。

引用哪些屬性同樣是通過一個 string literal 的 union 來定義的。這個union必須是 keyof 舊類型 的子集,可以是一個或多個 string literal,也可以是keyof的返回值 (即映射全部屬性)。

interface A {n k1: string;n k2: string;n k3: number;n}nn// 從A中取一部分屬性,類型不變 (A[P] 是上面講的查找類型)n// 結果: type A_var1 = { k1: string, k3: number }ntype A_var1 = {n [P in "k1" | "k3"]: A[P];n}nn// 從A中取所有屬性, 類型改為numbern// 結果: type A_var1 = { k1: number, k2: number, k3: number }n// **注意** keyof / Mapped type / 泛型一起使用時有一些特殊規則。建議讀一下最後一部分 "DeepReadonly 是怎樣展開的"ntype A_var2 = {n [P in keyof A]: number;n}nn// 從A中取所有屬性, 類型改為相應的Promise (TS 2.1 release note中的Deferred是這個的泛型版)ntype A_var3 = {n [P in keyof A]: Promise<A[P]>;n}n

新特性的例子: Readonly

使用上面介紹的新特性可以定義出一些可用作 類型的 decorator 的泛型,比如下面的 Readonly (已經在TS2.1標準庫中):

/**n * Make all properties in T readonlyn */ntype Readonly<T> = {n readonly [P in keyof T]: T[P];n};nninterface A {n k1: string;n k2: string;n k3: number;n}nn/**n 類型運算的結果為ntype A_ro = {n readonly k1: string;n readonly k2: string;n readonly k3: number;n}n */ntype A_ro = Readonly<A>;n

利用這些類型運算,我們可以表達出更複雜的編譯期約束,十分適合 (需要和無限的類型一起工作的) 的代碼或庫。比如 Release note 中還提到的Partial / Pick / Record 等類型。

Readonly的強化版: DeepReadonly

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

const v = { k1: 1, k2: { k21: 2 } };nnconst v_ro = v as Readonly<typeof v>;nn// 屬性: 不可賦值nv_ro.k1 = 2; n// 屬性的屬性: 可以賦值nv_ro.k2.k21 = 3;n

我們可以寫一個DeepReadonly,實現遞歸的只讀:

type DeepReadonly<T> = {n readonly [P in keyof T]: DeepReadonly<T[P]>;n};nnconst v_deep_ro = v as any as DeepReadonly<typeof v>;n// 屬性: 不可賦值nv_deep_ro.k1 = 2;n// 屬性的屬性: 也不可賦值nv_deep_ro.k2.k21 = 3;n

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 } }>n

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

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

↓ (規則1)

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

(規則1有時會導致一些不直觀的結果,不過大多數情況下我們不是想要基本類型的同構類型,到此停止展開可以接受)


推薦閱讀:

TypeScript入門
是時候再給TypeScript一次機會了【譯】
你所不知道的 Typescript 與 Redux 類型優化
推斷函數返回值的類型
Hello RxJS

TAG:TypeScript |