編寫高質量可維護的代碼:Awesome TypeScript

這是第 84 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客: 編寫高質量可維護的代碼:Awesome TypeScript

前言

高質量可維護的代碼應具有可讀性高、結構清晰、低耦合、易擴展等特色。而原生的 JavaScript 因爲其弱類型和沒有模塊化的缺點,不利於大型應用的開發和維護,所以,TypeScript 也就應運而生。前端

TypeScript 是 JavaScript 的一個超集,它的設計初衷並非爲了替代 JavaScript,而是基於 JavaScript 作了一系列的加強,包括增長了靜態類型、接口、類、泛型、方法重載等等。因此,只要你有必定的 JavaScript 功底,那麼 TypeScript 上手就很是簡單。而且,你能夠在 TypeScript 中愉快的使用 JavaScript 語法。typescript

接下去,本文將給你們分享下,TypeScript 的重要特性以及在實際場景中的使用技巧,幫助你們更高效的編寫高質量可維護的代碼。編程

Typescript VS Javascript

JavaScriptjson

  • JavaScript 是動態類型語言,在代碼編譯階段不會對變量進行類型檢測,從而會把潛在的類型錯誤帶到代碼執行階段。而且在遇到不一樣類型變量的賦值時,會自動進行類型轉換,帶來了不肯定性,容易產生 bug。
  • JavaScript 原生沒有命名空間,須要手動建立命名空間,來進行模塊化。而且,JavaScript 容許同名函數的重複定義,後面的定義能夠覆蓋前面的定義。這也給咱們開發和維護大型應用帶來了不便。

TypeScript數組

  • TypeScript 是靜態類型語言,經過類型註解提供編譯時的靜態類型檢查。安全

    • 在代碼編譯階段會進行變量的類型檢測,提早暴露潛在的類型錯誤問題。而且在代碼執行階段,不容許不一樣類型變量之間的賦值。
    • 清晰的類型註解,不只讓代碼的可讀性更好,同時也加強了 IDE 的能力,包括代碼補全、接口提示、跳轉到定義等等。
  • TypeScript 增長了模塊類型,自帶命名空間,方便了大型應用的模塊化開發。
  • TypeScript 的設計一種徹底面向對象的編程語言,具有模塊、接口、類、類型註解等,可讓咱們的代碼組織結構更清晰。

通過上述對比,能夠看到 TypeScript 的出現很好的彌補了 JavaScript 的部分設計缺陷,給咱們帶來了很大的便利,也提升了代碼的健壯性和擴展性。閉包

重要特性

數據類型

  • 基礎數據類型包括:Boolean、Number、String、Array、Enum、Any、Unknown、Tuple、Void、Null、Undefined、Never。下面選擇幾個 TypeScript 特有的類型進行詳解:編程語言

    • Enum 枚舉:在編碼過程當中,要避免使用硬編碼,若是某個常量是能夠被一一列舉出來的,那麼就建議使用枚舉類型來定義,可讓代碼更易維護。
    // 包括 數字枚舉、字符串枚舉、異構枚舉(數字和字符串的混合)。
    // 數字枚舉在不設置默認值的狀況下,默認第一個值爲0,其餘依次自增加
    enum STATUS {
      PENDING,
      PROCESS,
      COMPLETED,
    }
    let status: STATUS = STATUS.PENDING;  // 0
    • Any 類型:不建議使用。Any 類型爲頂層類型,全部類型均可以被視爲 any 類型,使用 Any 也就等同於讓 TypeScript 的類型校驗機制失效。
    • Unknown 類型:Unknown 類型也是頂層類型,它能夠接收任何類型,但它與 Any 的區別在於,它首次賦值後就肯定了數據類型,不容許變量的數據類型進行二次變動。因此,在須要接收全部類型的場景下,優先考慮用 Unknown 代替 Any。ide

      • Tuple 元組:支持數組內存儲不一樣數據類型的元素,讓咱們在組織數據的時候更靈活。
    let tupleType: [string, boolean];
    tupleType = ["momo", true];
    • Void 類型:當函數沒有返回值的場景下,一般將函數的返回值類型設置爲 void。

類型註解

  • TypeScript 經過類型註解提供編譯時的靜態類型檢查,能夠在編譯階段就發現潛在 Bug,同時讓編碼過程當中的提示也更智能。使用方式很簡單,在 : 冒號後面註明變量的類型便可。
const str: string = 'abc';

接口

  • 在面向對象編程的語言裏面,接口是實現程序解耦的關鍵,它只定義具體包含哪些屬性和方法,而不涉及任何具體的實現細節。接口是基於類之上,更進一步對實體或行爲進行抽象,會讓程序具有更好的擴展性。
  • 應用場景:好比咱們在實現訂單相關功能的時候,須要對訂單進行抽象,定義一個訂單的接口,包括訂單基本信息以及對訂單的相關操做,而後基於這個接口來作進一步的實現。後續若是訂單的相關操做功能有變化,只須要從新定義一個類來實現這個接口便可。模塊化

    interface Animal {
    name: string;
    getName(): string;
    }
    class Monkey implements Padder {
    constructor(private name: string) {
      getName() {
        return 'Monkey: ' + name;
        }
    }
    }

  • TypeScript 的類除了包括最基本的屬性和方法、getter 和 setter、繼承等特性,還新增了私有字段。私有字段不能在包含的類以外訪問,甚至不能被檢測到。Javascript 的類中是沒有私有字段的,若是想模擬私有字段的話,必需要用閉包來模擬。下面用一些示例來講明下類的使用:
  • 屬性和方法

    class Person {
    // 靜態屬性
    static name: string = "momo";
    // 成員屬性
    gender: string;
    // 構造函數
    constructor(str: string) {
      this.gender = str;
    }
    // 靜態方法
    static getName() {
      return this.name;
    }
    // 成員方法
    getGender() {
      return 'Gender: ' + this.gender;
    }
    }
    let person = new Person("female");
  • getter 和 setter

    • 經過 getter 和 setter 方法來實現數據的封裝和有效性校驗,防止出現異常數據。
    class Person {
    private _name: string;
    get name(): string {
      return this._name;
    }
    set name(newName: string) {
      this._name = newName;
    }
    }
    let person = new Person('momo');
    console.log(person.name); // momo
    person.name = 'new_momo';
    console.log(person.name); // new_momo
  • 繼承

    class Animal {
    name: string;
    constructor(nameStr=:string) {
      this.name = nameStr;
    }  
    move(distanceInMeters: number = 0) {
      console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
    }
    class Snake extends Animal {
    constructor(name: string) {
      super(name);
    } 
    move(distanceInMeters = 5) {
      super.move(distanceInMeters);
    }
    }
    let snake = new Snake('snake');
    snake.move(); // 輸出:'snake moved 5m'
  • 私有字段

    • 私有字段以 # 字符開頭。私有字段不能在包含的類以外訪問,甚至不能被檢測到。
    class Person {
    #name: string;
    constructor(name: string) {
      this.#name = name;
    }
    greet() {
         console.log(`Hello, ${this.#name}!`);
    }
    }
    let person = new Person('momo');
    person.#name;   // 訪問會報錯

泛型

  • 應用場景:當咱們須要考慮代碼的可複用性時,就須要用到泛型。讓組件不只可以支持當前的數據類型,同時也能支持將來的數據類型。泛型容許同一個函數接受不一樣類型參數,相比於使用 Any 類型,使用泛型來建立的組件可複用和易擴展性要更好,由於泛型會保留參數類型。泛型能夠應用於接口、類、變量。下面用一些示例來講明下泛型的使用:
  • 泛型接口

    interface identityFn<T> {
      (arg: T): T;
    }
  • 泛型類

    class GenericNumber<T> {
      zeroValue: T;
      add: (x: T, y: T) => T;
    }
    let myGenericNumber = new GenericNumber<number>();
    myGenericNumber.zeroValue = 0;
    myGenericNumber.add = function (x, y) {
      return x + y;
    };
  • 泛型變量

    使用大寫字母 A-Z 定義的類型變量都屬於泛型,常見泛型變量以下:

    • T(Type):表示一個 TypeScript 類型
    • K(Key):表示對象中的鍵類型
    • V(Value):表示對象中的值類型
    • E(Element):表示元素類型

交叉類型

  • 交叉類型就是將多個類型合併爲一個類型。經過 & 運算符定義。以下示例中,將 Person 類型和 Company 類型合併後,生成了新的類型 Staff,該類型同時具有這兩種類型的全部成員。

    interface Person {
    name: string;
    gender: string;
    }
    interface Company {
    companyName: string;
    }
    type Staff = Person & Company;
    const staff: Staff = {
    name: 'momo',
    gender: 'female',
    companyName: 'ZCY'
    };

聯合類型

  • 聯合類型就是由具備或關係的多個類型組合而成,只要知足其中一個類型便可。經過 | 運算符定義。以下示例中,函數的入參爲 string 或 number 類型便可。

    function fn(param: string | number): void {
      console.log("This is the union type");
    }

類型保護

  • 類型保護就是在咱們已經識別到當前數據是某種數據類型的狀況下,安全的調用這個數據類型對應的屬性和方法。經常使用的類型保護包括 in 類型保護、typeof 類型保護、instanceof 類型保護和 自定義 類型保護。具體見如下示例:

    • in 類型保護

      interface Person {
        name: string;
        gender: string;
      }
      interface Employee {
        name: string;
        company: string;
      }
      type UnknownStaff = Person | Employee;
      function getInfo(staff: UnknownStaff) {
        if ("gender" in staff) {
          console.log("Person info");
        }
        if ("company" in staff) {
          console.log("Employee info");
        }
      }
    • typeof 類型保護

      function processData(param: string | number): unknown {
          if (typeof param === 'string') {
            return param.toUpperCase()
        }
        return param;
      }
      • instanceof 類型保護:和 typeof 類型用法類似,它主要是用來判斷是不是一個類的對象或者繼承對象的。
      function processData(param: Date | RegExp): unknown {
          if (param instanceof Date) {
            return param.getTime();
        }
        return param;
      }
      • 自定義 類型保護:經過類型謂詞 parameterName is Type 來實現自定義類型保護。以下示例,實現了接口的請求參數的類型保護。
      interface ReqParams {
          url: string;
           onSuccess?: () => void;
           onError?: () => void;
      }
      // 檢測 request 對象包含參數符合要求的狀況下,才返回 url
      function validReqParams(request: unknown): request is ReqParams {
          return request && request.url
      }

開發小技巧

  • 須要連續判斷某個對象裏面是否存在某個深層次的屬性,可使用 ?.
if(result && result.data && result.data.list) // JS
if(result?.data?.list) // TS
  • 聯合判斷是否爲空值,可使用 ??
let temp = (val !== null && val !== void 0 ? val : '1'); // JS
let temp = val ?? '1'; // TS
  • 不要徹底依賴於類型檢查,必要時仍是須要編寫兜底的防護性代碼。

    • 由於類型報錯不會影響代碼生成和執行,因此原則上仍是會存在 fn('str') 調用的可能性,因此須要 default 進行兜底的防護性代碼。
function fn(value:boolean){
    switch(value){
      case true: 
        console.log('true');
      break;
    case false: 
      console.log('false');
      break;
    default: 
      console.log('dead code');
  }
}
  • 對於函數,要嚴格控制返回值的類型.
// 推薦寫法
function getLocalStorage<T>(key: string): T | null {
  const str = window.localStorage.getItem(key);
  return str ? JSON.parse(str) : null;
}
const data = getLocalStorage<DataType>("USER_KEY");
  • 利用 new() 實現工廠模式

    • TypeScript 語法實現工廠模式很簡單,只需先定義一個函數,並聲明一個構造函數的類型參數,而後在函數體裏面返回 c 這個類構造出來的對象便可。如下示例中,工廠函數構造出來的是 T 類型的對象。
function create<T>(c: { new(): T }): T {
    return new c();
}
class Test {
  constructor() {
  }
}
create(Test);
  • 優先考慮使用 Unknown 類型而非 Any
  • 使用 readonly 標記入參,保證參數不會在函數內被修改
function fn(arr:readonly number[] ){
  let sum=0, num = 0;
  while((num = arr.pop()) !== undefined){
    sum += num;
  }
  return sum;
}
  • 使用 Enum 維護常量表,實現更安全的類型檢查
// 使用 const enum 維護常量
const enum PROJ_STATUS {
  PENDING = 'PENDING',
  PROCESS = 'PROCESS',
  COMPLETED = 'COMPLETED'
}
function handleProject (status: PROJ_STATUS): void {
}
handleProject(PROJ_STATUS.COMPLETED)
  • 建議開啓如下編譯檢查選項,便於在編譯環境發現潛在 Bug
{
  "compilerOptions": {
    /* 嚴格的類型檢查選項 */
    "strict": true,                    // 啓用全部嚴格類型檢查選項
    "noImplicitAny": true,             // 在表達式和聲明上有隱含的 any類型時報錯
    "strictNullChecks": true,          // 啓用嚴格的 null 檢查
    "noImplicitThis": true,            // 當 this 表達式值爲 any 類型的時候,生成一個錯誤
    "alwaysStrict": true,              // 以嚴格模式檢查每一個模塊,並在每一個文件里加入 'use strict'
    
    /* 額外的檢查 */
    "noUnusedLocals": true,            // 有未使用的變量時,拋出錯誤
    "noUnusedParameters": true,        // 有未使用的參數時,拋出錯誤
    "noImplicitReturns": true,         // 並非全部函數裏的代碼都有返回值時,拋出錯誤
    "noFallthroughCasesInSwitch": true,// 報告 switch 語句的 fallthrough 錯誤。(即,不容許 switch 的 case 語句貫穿)
  }
}

相關 VSCode 插件推薦

TypeScript Extension Pack,它集合了咱們平常經常使用的 TypeScript 相關插件:

  • TSLint:自動檢測和修復不符合規範的 TypeScript 代碼。
  • TypeScript Hero:對 import 引入模塊順序進行排序和組織 ,移除未被使用的。MacOS 上快捷鍵 Ctrl+Opt+o,Win/Linux 上快捷鍵 Ctrl+Alt+o
  • json2ts:將剪切板中的 JSON 轉化成 TypeScript 接口。MacOS 上快捷鍵 Ctrl+Opt+V,Win/Linux 上快捷鍵 Ctrl+Alt+V
  • Move TS:在移動 TypeScript 文件或者包含 TypeScript 文件的文件夾時,會自動更新相關依賴模塊的 import 路徑。
  • Path Intellisense:路徑和文件名的自動提示補全功能。
  • TypeScript Importer:import 引入模塊時,自動搜索當前 workspace 下全部 export 的模塊,並自動進行提示補全。
  • Prettier - Code formatter:格式化代碼。

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索