Vue3都要上的typeScript之工程實踐

0. 前言

怎麼上... 咳咳,你們別想歪,這是一篇純技♂術文章。javascript

0.1 Why TypeScript

什麼?尤大要把Vue 3.0所有改爲用Typescript來寫?這不是逗我嗎,那我是否是要用TypeScript來寫Vue應用了? html

image.png

好吧,Vue3.0可能最快也要19年年底纔出來,Vue3.0是會對Ts使用者更友好,而不是隻能用ts了,尤大使用ts的緣由也是由於ts的靜態類型檢測以及ts的表現比flow愈來愈好了。自從巨硬大步邁向開源,前端圈子多了不少新工具好比VS Code、TypeScript。我的認爲TypeScript真正火起來仍是由於前端應用的複雜度不斷飆升,這帶來的問題就是維護性以及擴展性會變差。尤爲在編寫類庫的時候,更是須要考慮各個類以及方法的複用性和擴展性,因此會使用到設計模式來優化代碼。還有更重要的就是,編碼效率的提升,靜態系統無疑是下降了調試bug的時間。前端

0.2 Advantages & Disadvantages

優勢java

  • 靜態類型系統,能夠藉助編譯器幫助在編譯期間處理錯誤,提早避免在運行時可能發生的錯誤,無形中提升了代碼的可靠性。
  • 其次是若是程序中肯定了數據類型,編譯器能夠針對這些信息對程序進行優化。(Typescript是編譯爲JavaScript,針對JS的基本數據類型進行優化)。
  • 社區上的工具不少, VS code的支持很是給力, 類型提示以及Reference標記都很贊,開發者工具和體驗能夠說是JS世界中作得作好。

缺點node

  • 學習曲線,對於沒有Java/C++等靜態語言背景的程序員可能會須要有適應期。
  • Typescript做爲靜態類型語言須要程序員依照契約編寫程序,爲每一個變量規定類型,除了Javascript自己的string、number等基本類型,還須要經過Interface關鍵字爲複合結構聲明類型。
  • 類型的聲明會增長更多代碼,在程序編寫過程當中,這些細節會將程序員的精力從業務邏輯上分散開來。
let foo = 123;
foo = '456'; // Error: cannot assign `string` to `number
  • TypeScript支持ES2015+的新特性,隨着標準的發展,新特性會被不斷加入TypeScript中,使用TypeScript能夠經過編譯來規避在一些版本不高的瀏覽器中使用新特性的風險。

1. 工程實踐


1.1 老生常談webpack配置

Webpack已經發布到版本4.41了,相信不少小夥伴已經上了webpack4了,Webpack4對typescript的支持也是8錯的,它最大的變化莫過於"零配置"以及將commonChunks plugin插件嵌入爲webpack內置。最新版本:
image.pngwebpack

  1. 首先是安裝TypeScript,TypeScript是JavaScript的超集,擁有不少原生沒有的特性或者說是語法糖,同時瀏覽器沒法直接運行它,須要有一個編譯的過程,即將TypeScript編譯爲JavaScript,因此須要先安裝typescript
npm install -g typescript
  1. 而後來試試編譯,本地安裝完以後,就能夠對後綴爲.ts的文件進行編譯,輸出爲標準的JavaScript文件。

假設咱們有一個用TypeScript編寫的Student類。git

class Student {
  private name: string;
  constructor(name: string) {            
    this.name = name;
  }
}

使用typescript compiler來編譯它程序員

tsc student.ts

編譯後的結果是根據編譯選項來生成的標準JavaScript文件。es6

var Student = /** @class */ (function () {
    function Student(name) {
        this.name = name;
    }
    return Student;
}());
  1. 命令行進行編譯適用於對單個或少許的typescript文件的狀況,若是要使用typescript來編寫大型應用或類庫,就須要配置webpack在構建的時候自動編譯整個項目。使用Webpack配置TypeScript項目,遵循的流程是:
TypeScript-->ES Next的Javascript版本-->兼容性較好的JavaScript。

379750.png

值得注意

以前已經安裝了TypeScript compiler,一般會在compiler option中指定typescript是要編譯到支持ES5/ES6/ES Next的JavaScript版本,可是在實踐中咱們還須要利用Babel這個結果再進行一次轉譯,這麼作的緣由有兩個。github

  1. TypeScript編譯器編譯的結果還不能直接用於生產環境,使用Babel能夠經過browserlist來轉譯出兼容性適用於生產環境的js代碼。
  2. Babel能夠引入polyfill,一般會把TypeScript的編譯目標設置爲ES Next,而後Babel能夠根據須要引入polyfill,使得最後生成的js代碼體積是最少的。
const path = require('path')
const webpack = require('webpack')
const config = {
  entry: './src/index.ts',
  module: {
    rules: [
      {
        // ts-loader: convert typescript to javascript(esnext),
        // babel-loader: converts javascript(esnext) to javascript(backward compatibility)
        test: /\.(tsx|ts)?$/,
        use: ['babel-loader', 'ts-loader'],
        exclude: /node_modules/
      },
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
    alias: {
      '@': path.resolve(__dirname, './src'),
      'mobx': path.resolve(__dirname, './node_modules/mobx/lib/mobx.es6.js')
    }
  },
}

1.2 Typescript 編譯器配置

簡單介紹一下typescript的編譯選項,一般會在這裏指定編譯目標JS版本,代碼的模塊化方式以及代碼的檢查規則等。

  • allowJS表示是否容許編譯JavaScript文件。
  • target表示ECMAScript目標版本,好比‘ESNext’、'ES2015'。
  • module表示模塊化的方式,好比'commonjs'、'umd'或'es2105'(es module)
  • moduleResolution表示的是模塊解析的策略,即告訴編譯器在哪裏找到當前模塊,指定爲'node'時,就採用nodejs的模塊解析策略,完整算法能夠在Node.js module documentation找到;當它的值指定爲'classic'時則採用TypeScript默認的解析策略,這種策略主要是爲了兼容舊版本的typescript。
  • strict是否啓動全部的嚴格類型檢查選型,包括'noImplicitAny','noImplicitThis'等。
  • lib表示編譯過程當中須要引入的庫文件的列表,根據實際應用場景來引入。
  • experimentalDecorators是爲了支持裝飾器語法的選項,由於在項目中使用了Mobx作狀態管理,因此須要啓用裝飾器語法。
  • include選項表示編譯的目錄
  • outDir表示編譯結果輸出的目錄。
{
    "compileOnSave": true,
    "compilerOptions": {
        "target": "esnext",
        "module": "esnext",
        "moduleResolution": "node",
        "sourceMap": true,
        "strict": true,
        "allowJs": true,
        "experimentalDecorators": true,
        "outDir": "./dist/",
        "lib": [
          "es2015", "dom", "es2016", "es2017", "dom.iterable", "scripthost", "webworker"
        ]
    },

    "include": [
        "src/**/*.ts"
    ]
}

1.3 tslint實踐

tslint是針對typescript的lint工具,相似eslint遵循Airbnb Style或Standard Style,eslint也能夠指定要遵循的typescript規範,目前在tslint官方,給出了三種內置預設,recommendedlatest以及all,省去了咱們去對tslint每條規則進行配置的麻煩。

  • recommended 是穩定版的規則集,通常的typescript項目中使用它比較好,遵循SemVer。
  • latest 會不斷更新以包含每一個TSLint版本中最新規則的配置,一旦TSLint發佈了break change,這個配置也會跟隨着一塊兒更新。
  • all 將全部規則配置爲最爲嚴格的配置。

tslint規則

tslint的規則是有嚴重性等級的劃分,每條規則能夠配置default error warningoff。tslint預設提供了不少在代碼實踐中提煉出來的規則,我認爲有下面若干的規則,咱們會常常遇到,或者須要關注一下。

  • only-arrow-functions 只容許使用箭頭函數,不容許傳統的函數表達式。
  • promise-function-async 任何返回promise的函數或方法,都應該使用'async'標識出來;
  • await-promise 在'await'關鍵字後面跟隨的值不是promise時會警告,規範咱們異步代碼的編寫。
  • no-console 禁止在代碼中使用'console'方法,便於去除無用的調試代碼。
  • no-debugger 禁止在代碼中使用'debugger'方法,同上。
  • no-shadowed-variable 當在局部做用域和外層做用域存在同名的變量時,稱爲shadowing,這會致使局部做用域會沒法訪問外層做用域中的同名變量。
  • no-unused-variable 不容許存在,未使用的變量、import或函數等。這個規則的意義在於避免編譯錯誤,同時由於聲明瞭變量卻不適用,也致使了讀者混淆。
  • max-line-length 要求每行的字數有限制;
  • quotemark 指定對字符串常量,使用的符號,通常指定'single';這個看團隊風格了。
  • prefer-const 儘量用'const'聲明變量,而不是'let',不會被重複賦值的變量,默認使用'const';

其餘規則你們能夠詳細看tslint官方文檔,使用lint能夠更好地規範代碼風格,保持團隊代碼風格的統一,避免容易致使編譯錯誤的問題以及提升可讀性和維護性。


tslint的特殊flags

咱們用ts寫代碼的時候,常常會遇到一行代碼的字數過長的狀況,此時可使用tslint提供的flag來使得該行不受規則的約束。

// tslint:disable-next-line:max-line-length

  private paintPopupWithFade<T extends THREE.Object3D>(paintObj: T, popupStyleoption: PopupStyleOption, userDataType: number) {

  //...

}

實際上,tslint提示是該行的字數違反了 max-line-length規則,此處能夠經過增長註釋 // tslint: disable-next-line: rulex來禁用這個規則。

2. Typescript類型系統避坑tips


2.1 "鴨子"類型

"鴨子"類型??(黑人問號), 第一次看到這名詞我也很懵逼, 其實它說的是結構型類型,而目前類型檢測主要分爲結構型(structural)類型以及名義型(nominal)類型。

interface Point2D {
  x: number;
  y: number;
}
interface Point3D {
  x: number;
  y: number;
  z: number;
}
var point2D: Point2D = { x:0, y: 10}
var point3D: Point3D = { x: 0, y: 10, z: 20}

function iTakePoint2D(point: Point2D) { /*do sth*/ }

iTakePoint2D(point2D); // 類型匹配
iTakePoint2D(point3D); // 類型兼容,結構類型
iTakePoint2D({ x:0 }); // 錯誤: missing information `y`

區別

  • 結構型類型中的類型檢測和判斷的依據是類型的結構,會看它有哪些屬性,分別是什麼類型;而不是類型的名稱或者類型的id。
  • 名義類型是靜態語言Java、C等語言所使用的,簡單來講就是,若是兩個類型的類型名不一樣,那麼這兩個類型就是不一樣的類型了,儘管兩個類型是相同的結構。
  • Typescript中的類型是結構型類型,類型檢查關注的是值的形狀,即鴨子類型duck typing, 並且通常經過interface定義類型,其實就是定義形狀與約束~ 因此定義interface實際上是針對結構來定義新類型。對於Typescript來講,兩個類型只要結構相同,那麼它們就是一樣的類型。

2.2 類型判斷/區分類型

知道了typescript是個'鴨子類型'後,咱們就會想到一個問題,ts這種鴨子類型怎麼判斷類型啊,好比下面這個例子:

public convertString2Image(customizeData: UserDataType) {
    if (Helper.isUserData(customizeData)) {
      const errorIcon = searchImageByName(this.iconImage, statusIconKey);
      if (errorIcon) {
        (customizeData as UserData).title.icon = errorIcon;
      }
    } else if (Helper.isUserFloorData(customizeData)) {
      // do nothing
    } else {
      // UserAlertData
      let targetImg;
      const titleIcon = (customizeData as UserAlertData)!.title.icon;
      if (targetImg) {
        (customizeData as UserAlertData).title.icon = targetImg;
      }
    }
    return customizeData;
  }

該方法是根據傳入的用戶數據來將傳入的icon字段用實際對應的圖片填充,customizeData是用戶數據,此時咱們須要根據不一樣類型來調用searchImageByName方法去加載對應的圖片,因此咱們此時須要經過一些類型判斷的方法在運行時判斷出該對象的類型。

基礎的類型判斷

基本的類型判斷方法咱們可能會想到typeofinstanceof,在ts中,其實也可使用這兩個操做符來判斷類型,好比:

  • 使用typeof判斷類型
function doSomething(x: number | string) {
  if(typeof x === 'string') {
      console.log(x.toFixed()); // Property 'toFixed' does not exist on type 'string'
      console.log(x.substr(1));
  } else if (typeof x === 'number') {
      console.log(x.toFixed());
      console.log(x.substr(1)); // Property 'substr' does not exist on type 'number'.
  }
}

能夠看到使用typeof在運行時判斷基礎數據類型是可行的,能夠在不一樣的條件塊中針對不一樣的類型執行不一樣的業務邏輯,可是對於Class或者Interface定義的非基礎類型,就必須考慮其餘方式了。

  • 使用instanceof判斷類型

下面這個例子根據傳入的geo對象的類型執行不一樣的處理邏輯:

public addTo(geo: IMap | IArea | Marker) {
    this.gisObj = geo;
    this.container = this.draw()!;
    if (!this.container) {
      return;
    }
    this.mapContainer.appendChild<HTMLDivElement>(this.container!);
    if (this.gisObj instanceof IMap) {
      this.handleDuration();
    } else if(this.gisObj instanceof Marker) {
      //
    }
  }

能夠看到,使用instanceof動態地判斷類型是可行的,並且類型能夠是Class關鍵字聲明的類型,這些類型都擁有複雜的結構,並且擁有構造函數。總地來講,使用instanceof判斷類型的兩個條件是:

  1. 必須是擁有構造函數的類型,好比類類型。
  2. 構造函數prototype屬性類型不能爲any

利用類型謂詞來判斷類型
結合一開始的例子,咱們要去判斷一個鴨子類型,在ts中,咱們有特殊的方式,就是類型謂詞(type predicate)的概念,這是typescript的類型保護機制,它會在運行時檢查確保在特定做用域內的類型。針對那些Interface定義的類型以及映射出來的類型,並且它並不具備構造函數,因此咱們須要本身去定義該類型的檢查方法,一般也被稱爲類型保護

例子中的調用的兩個基於類型保護的方法的實現

public static isUserData(userData: UserDataType): userData is UserData {
    return ((userData as UserData).title !== undefined) && ((userData as UserData).subTitle !== undefined)
      && ((userData as UserData).body !== undefined) && ((userData as UserData).type === USER_DATA_TYPE.USER_DATA);
  }
  public static isUserFloorData(userFloorData: UserDataType): userFloorData is UserFloorData {
    return ((userFloorData as UserFloorData).deviceAllNum !== undefined)
      && ((userFloorData as UserFloorData).deviceNormalNum !== undefined)
      && ((userFloorData as UserFloorData).deviceFaultNum !== undefined)
      && ((userFloorData as UserFloorData).deviceOfflineNum !== undefined);
  }

實際上,咱們要去判斷這個類型的結構,這也是爲何ts的類型系統被稱爲鴨子類型,咱們須要遍歷對象的每個屬性來區分類型。換句話說,若是定義了兩個結構徹底相同的類型,即使類型名不一樣也會判斷爲相同的類型~

2.3 索引類型幹嗎用?

索引類型(index types),使用索引類型,編譯器就可以檢查使用了動態屬性名的代碼。ts中經過索引訪問操做符keyof獲取類型中的屬性名,好比下面的例子:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n => o[n]);
}
​
interface Person {
  name: string;
  age: number;
}
let person: Person {
  name: 'Jarid',
  age: 35
}
let strings: string[] = pluck(person, ['name']);

原理
編譯器會檢查name是否真的爲person的一個屬性,而後keyof T,索引類型查詢操做符,對於任何類型T, keyof T的結果爲T上已知的屬性名的聯合。

let personProps: keyof Person; // 'name' | 'age'

也就是說,屬性名也能夠是任意的interface類型!

索引訪問操做符T[K]

索引類型指的其實ts中的屬性能夠是動態類型,在運行時求值時才知道類型。你能夠在普通的上下文中使用T[K]類型,只須要確保K extends keyof T便可,例以下面:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name];
}

原理:o:Tname:K 表示o[name]: T[K]  當你返回T[K] 的結果,編譯器會實例化key的真實類型,所以getProperty的返回值的類型會隨着你須要的屬性改變而改變。

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

索引類型和字符串索引簽名
keyofT[k] 與字符串索引簽名進行交互。 
好比:

interface Map<T> {
    [key: string]: T; // 這是一個帶有字符串索引簽名的類型, keyof T 是 string
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

Map<T>是一個帶有字符串索引簽名的類型,那麼keyof T 會是string。

2.4 映射類型

背景
在使用typescript時,會有一個問題咱們是繞不開的 --> 如何從舊的類型中建立新類型即映射類型。

interface PersonPartial {
    name?: string;
    age?: number;
}

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

能夠看到PersonReadOnly這個類型僅僅是對PersonParial類型的字段只讀化設置,想象一下 若是這個類型是10個字段那就須要重複寫這10個字段。咱們有沒辦法不去重複寫這種樣板代碼,而是經過映射獲得新類型? 答案就是映射類型,

映射類型的原理
 新類型以相同的形式去轉換舊類型裏每一個屬性:

type Readonly<T> {
   readonly [P in keyof T]: T[P];
}

它的語法相似於索引簽名的語法,有三個步驟:

  1. 類型變量K, 依次綁定到每一個屬性。
  2. 字符串字面量聯合的Keys,包含了要迭代的屬性名的集合
  3. 屬性的類型。

好比下面這個例子

type Keys = 'option1' | 'option2';
type Flags = { [K in keys]: boolean };

Keys,是硬編碼的一串屬性名,而後這個屬性的類型是boolean,所以這個映射類型等同於:

type Flags = {
    option1: boolean;
    option2: boolean;
}

典型用法
咱們常常會遇到的或者更通用的是(泛型的寫法):

type Nullable<T> = { [P in keyof T]: T[P] | null }

聲明一個Person類型,一旦用Nullable類型轉換後,獲得的新類型的每個屬性就是容許爲null的類型了。

// test
interface Person {
    name: string;
    age: number;
    greatOrNot: boolean;
}
type NullPerson = Nullable<Person>;

const nullPerson: NullPerson = {
    name: '123',
    age: null,
    greatOrNot: true,
};

騷操做
利用類型映射,咱們能夠作到對類型的PickOmitPick是ts自帶的類型,好比下面的例子:

export interface Product {
  id: string;
  name: string;
  price: string;
  description: string;
  author: string;
  authorLink: string;
}

export type ProductPhotoProps = Pick<Product, 'id' | 'author'| 'authorlink' | 'price'>;

// Omit的實現
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

export type ProductPhotoOtherProps = Omit<Product, 'name' | 'description'>;

咱們能夠把已有的Product類型中的若干類型pick出來組成一個新類型;也能夠把若干的類型忽略掉,把剩餘的屬性組成新的類型。

好處

  • keyof T返回的是T的屬性列表,T[P]是結果類型,這種類型轉換不會應用到原型鏈上的其餘屬性,意味着映射只會應用到T的屬性上而不會在原型鏈的其餘屬性上。編譯器會在添加新屬性以前拷貝全部存在的屬性修飾符。
  • 無論是屬性或者方法均可以被映射。

2.5 Never類型 vs Void類型

never
首先,never類型有兩種場景:

  • 做爲函數返回值時是表示永遠不會有返回值的函數。
  • 表示一個老是拋出錯誤的函數。
// 返回never的函數必須存在沒法達到的終點
function error(message: string): never {
    throw new Error(message);
}
// 推斷的返回值類型爲never
function fail() {
    return error("Something failed");
}

void
void也有它的應用場景

  • 表示的是沒有任何類型,當一個函數沒有返回值時,一般typescript會自動認爲它的返回值時void
  • 在代碼中聲明void類型或者返回值標記爲void能夠提升代碼的可讀性,讓人明確該方法是不會有返回值,寫測試時也能夠避免去關注返回值。
public remove(): void {
    if (this.container) {
      this.mapContainer.removeChild(this.container);
    }
    this.container = null;
  }

小結

  • never實質表示的是那些永遠不存在值的類型,也能夠表示函數表達式或箭頭函數表達式的返回值。
  • 咱們能夠定義函數或變量爲void類型,變量仍然能夠被賦值undefinednull,可是never是隻能被返回值爲never的函數賦值。

2.6 枚舉類型

ts中用enum關鍵字來定義枚舉類型,彷佛在不少強類型語言中都有枚舉的存在,然而Javascrip沒有,枚舉能夠幫助咱們更好地用有意義的命名去取代那些代碼中常常出現的magic number或有特定意義的值。這裏有個在咱們的業務裏用到的枚舉類型:

export enum GEO_LEVEL {
  NATION = 1,
  PROVINCE = 2,
  CITY = 3,
  DISTRICT = 4,
  BUILDING = 6,
  FLOOR = 7,
  ROOM = 8,
  POINT = 9,
}

由於值都是number,通常也被稱爲數值型枚舉。

基於數值的枚舉
ts的枚舉都是基於數值類型的,數值能夠被賦值到枚舉好比:

enum Color {
    Red,
    Green,
    Blue
}
var col = Color.Red;
col = 0; // 與Color.Red的效果同樣

ts內部實現
咱們看看上面的枚舉值爲數值類型的枚舉類型會怎樣被轉爲JavaScript:

// 轉譯後的Javascript
define(["require", "exports"], function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    var GEO_LEVEL;
    (function (GEO_LEVEL) {
        GEO_LEVEL[GEO_LEVEL["NATION"] = 1] = "NATION";
        GEO_LEVEL[GEO_LEVEL["PROVINCE"] = 2] = "PROVINCE";
        GEO_LEVEL[GEO_LEVEL["CITY"] = 3] = "CITY";
        GEO_LEVEL[GEO_LEVEL["DISTRICT"] = 4] = "DISTRICT";
        GEO_LEVEL[GEO_LEVEL["BUILDING"] = 6] = "BUILDING";
        GEO_LEVEL[GEO_LEVEL["FLOOR"] = 7] = "FLOOR";
        GEO_LEVEL[GEO_LEVEL["ROOM"] = 8] = "ROOM";
        GEO_LEVEL[GEO_LEVEL["POINT"] = 9] = "POINT";
    })(GEO_LEVEL = exports.GEO_LEVEL || (exports.GEO_LEVEL = {}));
});

很是有趣,咱們先不去想爲何要這麼轉譯,換個角度思考,其實上面的代碼說明了這樣一個事情:

console.log(GEO_LEVEL[1]); // 'NATION'
console.log(GEO_LEVEL['NATION']) // 1
// GEO_LEVEL[GEO_LEVEL.NATION] === GEO_LEVEL[1]

因此其實咱們能夠經過這個枚舉變量GEO_LEVEL去將下標表示的枚舉轉爲key表示的枚舉,key表示的枚舉也能夠轉爲用下標表示。

3. Reference

design pattern in typescript

typescript deep dive

tslint rules

typescript中文文檔

typescript 高級類型

you might not need typescript

advanced typescript classes and types

相關文章
相關標籤/搜索