去年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有时会导致一些不直观的结果,不过大多数情况下我们不是想要基本类型的同构类型,到此停止展开可以接受)