在 TS 中如何減小重複代碼

相信有些讀者已經據說過 DRY 原則,DRY 的全稱是 —— Don't Repeat Yourself ,是指編程過程當中不寫重複代碼,將可以公共的部分抽象出來,封裝成工具類或者用抽象類來抽象公共的東西,從而下降代碼的耦合性,這樣不只提升代碼的靈活性、健壯性以及可讀性,也方便後期的維護。node

接下來,本文將介紹在 TypeScript 項目開發過程當中,如何參考 DRY 原則儘可能減小重複代碼。減小重複的最簡單方法是命名類型,而不是經過如下這種方式來定義一個 distance 函數:typescript

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 接口:編程

interface Point2D { 
  x: number;
  y: number;
}

function distance(a: Point2D, b: Point2D) { /* ... */ }
複製代碼

然而在實際的開發過程當中,重複的類型並不老是那麼容易被發現。有時它們會被語法所掩蓋。若是多個函數共享相同的類型簽名,好比:微信

function get(url: string, opts: Options): Promise<Response> { /* ... */ } 
function post(url: string, opts: Options): Promise<Response> { /* ... */ }
複製代碼

對於上面的 get 和 post 方法,爲了不重複的代碼,咱們能夠提取統一的類型簽名:函數

type HTTPFunction = (url: string, opts: Options) => Promise<Response>; 

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

對於 TypeScript 初學者來講,在定義接口的時候也要當心,避免出現如下相似的重複代碼。好比:工具

interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate {
  firstName: string;
  lastName: string;
  birth: Date;
}
複製代碼

很明顯,相對於 Person 接口來講,PersonWithBirthDate 接口只是多了一個 birth 屬性,其餘的屬性跟 Person 接口是同樣的。那麼如何避免出現例子中的重複代碼呢?要解決這個問題,能夠利用 extends 關鍵字:post

interface Person { 
  firstName: string; 
  lastName: string;
}

interface PersonWithBirthDate extends Person { 
  birth: Date;
}
複製代碼

固然除了使用 extends 關鍵字以外,也可使用交叉運算符(&):優化

type PersonWithBirthDate = Person & { birth: Date };
複製代碼

下面咱們來繼續看另外一個例子,假設你已經定義 State(表明整個應用程序的狀態)和 TopNavState(只表明部分應用程序的狀態)兩個接口:ui

interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}

interface TopNavState {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
}
複製代碼

上述的 TopNavState 接口相比 State 接口只是缺乏了 pageContents 屬性,但咱們卻重複聲明其餘三個相同的屬性。爲了減小重複代碼,咱們能夠這樣作:url

type TopNavState = {
  userId: State['userId']; 
  pageTitle: State['pageTitle']; 
  recentFiles: State['recentFiles'];
};
複製代碼

在上面代碼中,咱們經過成員訪問的語法來提取對象中屬性的類型,從而避免重複定義接口中相關屬性的類型。但這並無解決本質的問題,咱們還有很大的優化空間。針對這個問題,咱們能夠利用映射類型來進一步作優化:

type TopNavState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles']: State[k]
};
複製代碼

鼠標懸停在 TopNavState 顯示它的聲明,實際上,這個定義與前一個定義徹底相同。

top-nav-state-definition

經過映射類型優化後的代碼,相比 TopNavState 接口最初的代碼簡潔了許多。那還有沒有優化空間呢?實際上是有的,咱們能夠利用 TypeScript 團隊爲咱們開發者提供的工具類型,這裏咱們可使用 Pick

type TopNavState = Pick<
  State, 'userId' | 'pageTitle' | 'recentFiles'
>;
複製代碼

其實除了 Pick 以外,在實際開發過程咱們還能夠利用其餘內置的工具類型來減小重複代碼。這裏咱們再來介紹另外一個比較經常使用的工具類型,即 Partial。如下是未使用 Partial 的例子:

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 的情形:

type OptionsUpdate = {[k in keyof Options]?: Options[k]};
複製代碼

keyof 操做符接受一個類型,並返回一個由 key 組成的聯合類型:

type OptionsKeys = keyof Options;
// Type is "width" | "height" | "color" | "label"
複製代碼

in 操做符是用來遍歷枚舉類型或聯合類型。接着,咱們來看一下使用 Partial 的情形:

class UIWidget {
  constructor(init: Options) { /* ... */ } 
  update(options: Partial<Options>) { /* ... */ }
}
複製代碼

其實 Partial 並無什麼神奇的地方,咱們來看一下它的定義:

// 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] 取得相應的屬性類型。中間的 ? 號,用於將全部屬性變爲可選。

有時候,你可能還會發現本身想要定義一個類型來匹配一個初始配置項的形狀,好比:

const INIT_OPTIONS = {
  width: 640,
  height: 480,
  color: "#00FF00",
  label: "VGA",
};

interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
複製代碼

對於 Options 接口來講,咱們還可使用 typeof 操做符來快速定義該接口類型:

type Options = typeof INIT_OPTIONS;
複製代碼

此外,在使用可辨識聯合(代數數據類型或標籤聯合類型)的過程當中,也可能出現重複代碼。好比:

interface SaveAction { 
  type: 'save';
  // ...
}

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

type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load'; // Repeated types!
複製代碼

爲了不重複定義 'save''load',咱們可使用前面提到的成員訪問語法,來提取對象中屬性的類型:

type ActionType = Action['type']; // 類型是 "save" | "load"
複製代碼

這裏須要注意的是,Action['type'] 返回的是聯合類型,而若是咱們使用前面介紹的 Pick 工具類型,它會返回一個含有 type 屬性的接口:

type ActionRec = Pick<Action, 'type'>; // {type: "save" | "load"}
複製代碼

本文經過一些簡單的示例,介紹了在 TypeScript 開發過程當中如何減小重複代碼,其實除了文中介紹了 PickPartial 以外,TypeScript 團隊還爲咱們開發者提供了不少工具類型,可用於減小重複代碼和提升開發效率,感興趣的讀者能夠閱讀本人以前寫的 掌握 TS 這些工具類型,讓你開發事半功倍 這篇文章。

建立了一個 「重學TypeScript」 的微信羣,想加羣的小夥伴,加我微信 "semlinker",備註重學TS。目前已有 TS 系列文章 38 篇。

相關文章
相關標籤/搜索