一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情。
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧14: 用类型操作,泛型避免重复代码
用命令行打印若干个长方体的: 长宽高, 表面积, 体积:
console.log('Cylinder 1 x 1 ',
'Surface area:', 6.283185 * 1 * 1 + 6.283185 * 1 * 1,
'Volume:', 3.14159 * 1 * 1 * 1);
console.log('Cylinder 1 x 2 ',
'Surface area:', 6.283185 * 1 * 1 + 6.283185 * 2 * 1,
'Volume:', 3.14159 * 1 * 2 * 1);
console.log('Cylinder 2 x 1 ',
'Surface area:', 6.283185 * 2 * 1 + 6.283185 * 2 * 1,
'Volume:', 3.14159 * 2 * 2 * 1);
这些代码看起来很不舒服, 因为重复特别多.不仅重复值还重复了很多常量. 很容易出现拼写错误. 更好的写法,将代码拆分成: 函数, 常量, 循环
const surfaceArea = (r, h) => 2 * Math.PI * r * (r + h);
const volume = (r, h) => Math.PI * r * r * h;
for (const [r, h] of [[1, 1], [1, 2], [2, 1]]) {
console.log(
`Cylinder ${r} x ${h}`,
`Surface area: ${surfaceArea(r, h)}`,
`Volume: ${volume(r, h)}`);
}
这就是 DRY 原则: 不要重复自己. 同时我们应该避免在写类型的时候重复:
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate {
firstName: string;
lastName: string;
birth: Date;
}
类型代码的重复和普通代码的重复会造成同样的问题. 但是重复的类型代码会更多见, 因为有许多人不熟悉如何对类型代码进行分解成: 函数, 循环.
-
抽取常量
最简单的方法减少类型代码重复, 抽取出 type 后重命名:
function distance(a: {x: number, y: number}, b: {x: number, y: number}) { return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); }抽取出一个 type, 重命名:
interface Point2D { x: number; y: number; } function distance(a: Point2D, b: Point2D) { /* ... */ }这相当于在类型系统中, 分解出一个常量, 而不是重复去写.但是复杂了类型的重复不容易被发现, 因为它们有时候会被复杂语法所隐藏.
如果多个函数类型一样, 也可以抽取type. 抽取前
function get(url: string, opts: Options): Promise<Response> { /* ... */ } function post(url: string, opts: Options): Promise<Response> { /* ... */ }抽取后:
type HTTPFunction = (url: string, opts: Options) => Promise<Response>; const get: HTTPFunction = (url, opts) => { /* ... */ }; const post: HTTPFunction = (url, opts) => { /* ... */ }; -
扩展.
对于
Person/PersonWithBirthDate那个例子, 我们可以用extends 来优化:interface Person { firstName: string; lastName: string; } interface PersonWithBirthDate extends Person { birth: Date; }当两个 interface 有部分属性相同, 我们可以抽取出一个含有相同属性的interface. 继续分析:
3.141593and6.283185应该用PIand2*PI来代替.也可以用交集操作符(&)来扩展 type:
type PersonWithBirthDate = Person & { birth: Date }; -
循环:
当有这样两个 interface:
interface State { userId: string; pageTitle: string; recentFiles: string[]; pageContents: string; } interface TopNavState { userId: string; pageTitle: string; recentFiles: string[]; }你希望将 TopNavState的字段 定义为 State 字段的子集, 而不是通过 extends TopNavState 来生成 State.
那么你可以通过索引来做:
type TopNavState = { userId: State['userId']; pageTitle: State['pageTitle']; recentFiles: State['recentFiles']; };这样依旧有部分重复代码, 我们可以用循环来改进:
type TopNavState = { [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k] }; -
泛型.
上一个部分我们可以用循环来生成type, 实际使用中我们会将泛型和循环结合起来.
这种方法在标准库中非常常见:type Pick<T, K> = { [k in K]: T[k] };我们可以这样使用:
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;泛型就好像我们普通代码的函数.
-
标记的联合: 另外一种常见的重复在于标记的联合, 例如:
interface SaveAction { type: 'save'; // ... } interface LoadAction { type: 'load'; // ... } type Action = SaveAction | LoadAction; type ActionType = 'save' | 'load'; // Repeated types!我们可以这样解决重复:
type ActionType = Action['type']; // Type is "save" | "load"这样使用和使用pick泛型得到的标记联合是不一样的:
type ActionRec = Pick<Action, 'type'>; // {type: "save" | "load"} -
update type 另外一种常见的重复: 我们定义了一个class, 随后还需要对这个class进行升级. 升级的参数有可能能是可选参数:
interface Options { width: number; height: number; color: string; label: string; } interface OptionsUpdate { width?: number; height?: number; color?: string; label?: string; } class UIWidget { constructor(init: Options) { /* ... */ } update(options: OptionsUpdate) { /* ... */ } }我们可以用循环来减少OptionsUpdate的重复:
type OptionsUpdate = {[k in keyof Options]?: Options[k]};keyof 用来获取标记的联合:
type OptionsKeys = keyof Options; // Type is "width" | "height" | "color" | "label"这种方法非常常见, 以至于抽象成一个泛型放入了标准库:
class UIWidget { constructor(init: Options) { /* ... */ } update(options: Partial<Options>) { /* ... */ } } -
由初始值快速生成type.
我们有时会需要初始值来生成相应的type:
const INIT_OPTIONS = { width: 640, height: 480, color: '#00FF00', label: 'VGA', }; interface Options { width: number; height: number; color: string; label: string; }我们可以这样生成:
type Options = typeof INIT_OPTIONS;注意: 小心从 values 中生成 types. 更安全的办法就是先定义type, 在定义values的时候指定type.
相似的, 你可能想根据函数的返回值得到一个type,例如:
function getUserInfo(userId: string) { // ... return { userId, name, age, height, weight, favoriteColor, }; } // Return type inferred as { userId: string; name: string; age: number, ... }标准库中有一个泛型 ReturnType 可以直接得到返回值的type:
type UserInfo = ReturnType<typeof getUserInfo>; -
对泛型的参数进行约束.
我们可以用 extends 对泛型的参数进行约束:
interface Name { first: string; last: string; } type DancingDuo<T extends Name> = [T, T]; const couple1: DancingDuo<Name> = [ {first: 'Fred', last: 'Astaire'}, {first: 'Ginger', last: 'Rogers'} ]; // OK const couple2: DancingDuo<{first: string}> = [ // ~~~~~~~~~~~~~~~ // Property 'last' is missing in type // '{ first: string; }' but required in type 'Name' {first: 'Sonny'}, {first: 'Cher'} ];我们还可以用泛型约束对之前的pick函数进一步改进:
type Pick<T, K extends keyof T> = { [k in K]: T[k] }; // O改进后我们能对参数实现更好的约束.:
type FirstLast = Pick<Name, 'first' | 'last'>; // OK type FirstMiddle = Pick<Name, 'first' | 'middle'>; // ~~~~~~~~~~~~~~~~~~ // Type '"middle"' is not assignable // to type '"first" | "last"'