前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在 TS 中如何减少重复代码

在 TS 中如何减少重复代码

作者头像
阿宝哥
发布2020-05-06 15:21:47
2.3K0
发布2020-05-06 15:21:47
举报
文章被收录于专栏:全栈修仙之路

相信有些读者已经听说过 DRY 原则,DRY 的全称是 —— Don’t Repeat Yourself ,是指编程过程中不写重复代码,将能够公共的部分抽象出来,封装成工具类或者用抽象类来抽象公共的东西,从而降低代码的耦合性,这样不仅提高代码的灵活性、健壮性以及可读性,也方便后期的维护。

接下来,本文将介绍在 TypeScript 项目开发过程中,如何参考 DRY 原则尽量减少重复代码。减少重复的最简单方法是命名类型,而不是通过以下这种方式来定义一个 distance 函数:

代码语言:txt
复制
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));
}

在上述的 distance 方法中,我们重复使用 {x: number, y: number} 来定义参数 a 和参数 b 的类型,要解决这个问题很简单,我们可以定义一个 Point2D 接口:

代码语言:txt
复制
interface Point2D { 
  x: number;
  y: number;
}

function distance(a: Point2D, b: Point2D) { /* ... */ }

然而在实际的开发过程中,重复的类型并不总是那么容易被发现。有时它们会被语法所掩盖。如果多个函数共享相同的类型签名,比如:

代码语言:txt
复制
function get(url: string, opts: Options): Promise<Response> { /* ... */ } 
function post(url: string, opts: Options): Promise<Response> { /* ... */ }

对于上面的 get 和 post 方法,为了避免重复的代码,我们可以提取统一的类型签名:

代码语言:txt
复制
type HTTPFunction = (url: string, opts: Options) => Promise<Response>; 

const get: HTTPFunction = (url, opts) => { /* ... */ };
const post: HTTPFunction = (url, opts) => { /* ... */ };

对于 TypeScript 初学者来说,在定义接口的时候也要小心,避免出现以下类似的重复代码。比如:

代码语言:txt
复制
interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate {
  firstName: string;
  lastName: string;
  birth: Date;
}

很明显,相对于 Person 接口来说,PersonWithBirthDate 接口只是多了一个 birth 属性,其他的属性跟 Person 接口是一样的。那么如何避免出现例子中的重复代码呢?要解决这个问题,可以利用 extends 关键字:

代码语言:txt
复制
interface Person { 
  firstName: string; 
  lastName: string;
}

interface PersonWithBirthDate extends Person { 
  birth: Date;
}

当然除了使用 extends 关键字之外,也可以使用交叉运算符(&):

代码语言:txt
复制
type PersonWithBirthDate = Person & { birth: Date };

下面我们来继续看另一个例子,假设你已经定义 State(代表整个应用程序的状态)和 TopNavState(只代表部分应用程序的状态)两个接口:

代码语言:txt
复制
interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}

interface TopNavState {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
}

上述的 TopNavState 接口相比 State 接口只是缺少了 pageContents 属性,但我们却重复声明其他三个相同的属性。为了减少重复代码,我们可以这样做:

代码语言:txt
复制
type TopNavState = {
  userId: State['userId']; 
  pageTitle: State['pageTitle']; 
  recentFiles: State['recentFiles'];
};

在上面代码中,我们通过成员访问的语法来提取对象中属性的类型,从而避免重复定义接口中相关属性的类型。但这并没有解决本质的问题,我们还有很大的优化空间。针对这个问题,我们可以利用映射类型来进一步做优化:

代码语言:txt
复制
type TopNavState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};

鼠标悬停在 TopNavState 显示它的声明,实际上,这个定义与前一个定义完全相同。

通过映射类型优化后的代码,相比 TopNavState 接口最初的代码简洁了许多。那还有没有优化空间呢?其实是有的,我们可以利用 TypeScript 团队为我们开发者提供的工具类型,这里我们可以使用 Pick

代码语言:txt
复制
type TopNavState = Pick<
  State, 'userId' | 'pageTitle' | 'recentFiles'
>;

其实除了 Pick 之外,在实际开发过程我们还可以利用其他内置的工具类型来减少重复代码。这里我们再来介绍另一个比较常用的工具类型,即 Partial。以下是未使用 Partial 的例子:

代码语言:txt
复制
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) {
    /* ... */
  }
}

在以上示例中,我们定义了 Options 和 OptionsUpdate 两个接口,它们分别用于描述 UIWidget 的初始化配置项和更新配置项。相比初始化配置项,更新配置项的所有属性都是可选的。

现在我们来开始优化上述的代码,我们先来看一下不使用 Partial 的情形:

代码语言:txt
复制
type OptionsUpdate = {[k in keyof Options]?: Options[k]};

keyof 操作符接受一个类型,并返回一个由 key 组成的联合类型:

代码语言:txt
复制
type OptionsKeys = keyof Options;
// Type is "width" | "height" | "color" | "label"

in 操作符是用来遍历枚举类型或联合类型。接着,我们来看一下使用 Partial 的情形:

代码语言:txt
复制
class UIWidget {
  constructor(init: Options) { /* ... */ } 
  update(options: Partial<Options>) { /* ... */ }
}

其实 Partial 并没有什么神奇的地方,我们来看一下它的定义:

代码语言:txt
复制
// node_modules/typescript/lib/lib.es5.d.ts

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性类型。中间的 ? 号,用于将所有属性变为可选。

有时候,你可能还会发现自己想要定义一个类型来匹配一个初始配置项的形状,比如:

代码语言:txt
复制
const INIT_OPTIONS = {
  width: 640,
  height: 480,
  color: "#00FF00",
  label: "VGA",
};

interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}

对于 Options 接口来说,我们还可以使用 typeof 操作符来快速定义该接口类型:

代码语言:txt
复制
type Options = typeof INIT_OPTIONS;

此外,在使用可辨识联合(代数数据类型或标签联合类型)的过程中,也可能出现重复代码。比如:

代码语言:txt
复制
interface SaveAction { 
  type: 'save';
  // ...
}

interface LoadAction {
  type: 'load';
  // ...
}

type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load'; // Repeated types!

为了避免重复定义 'save''load',我们可以使用前面提到的成员访问语法,来提取对象中属性的类型:

代码语言:txt
复制
type ActionType = Action['type']; // 类型是 "save" | "load"

这里需要注意的是,Action['type'] 返回的是联合类型,而如果我们使用前面介绍的 Pick 工具类型,它会返回一个含有 type 属性的接口:

代码语言:txt
复制
type ActionRec = Pick<Action, 'type'>; // {type: "save" | "load"}

本文通过一些简单的示例,介绍了在 TypeScript 开发过程中如何减少重复代码,其实除了文中介绍了 PickPartial 之外,TypeScript 团队还为我们开发者提供了很多工具类型,可用于减少重复代码和提高开发效率,感兴趣的读者可以阅读本人之前写的 掌握 TS 这些工具类型,让你开发事半功倍 这篇文章。


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/05/05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档