去年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有时会致使一些不直观的结果,不过大多数状况下咱们不是想要基本类型的同构类型,到此中止展开能够接受)