天天看點

【萬字長文】深入了解 Typescript 進階用法

「前言」:這裡的标題看起來是 "進階用法",不少同學可能就表示被勸退了。其實

Typescript

作為一門

強類型

程式設計語言,最具特色的就是他的類型表達能力,這是很多完備的後端語言都難以媲美的 說的很對,但PHP是最好的語言,是以如果你搞懂了他的類型系統,對将來的日常開發一定是大有裨益的,但過于靈活的類型系統也注定了

Typescript

無法成為一門純粹的靜态語言,不過每一行代碼都有代碼提示他不香嘛?

大綱

    • 基礎準備
    • Typescript 類型系統簡述
  • Typescript 的類型是支援定義 "函數定義" 的
  • Typescript 的類型是支援 "條件判斷" 的
  • Typescript 的類型是支援 "資料結構" 的
  • Typescript 的類型是支援 "作用域" 的
  • Typescript 的類型是支援 "遞歸" 的
  • 小結
    • "進階用法" 的使用場景與價值
  • 哪些用法可以被稱為 "進階用法"
  • 舉例說明 "進階用法" 的使用場景
    • 類型推導與泛型操作符
  • 流動的類型(類型編寫思路)
  • Typescript 代碼哲學
    • 常見類型推導實作邏輯梳理
  • 類型的傳遞(流動)
  • 類型的過濾與分流
    • 定制化擴充你的 Typescript
  • Typescript Service Plugins 的産生背景、功能定位、基礎使用
  • 市面上已有的 Typescript Service Plugins 舉例介紹
  • 參考資料連結
    • Q&A
  • 可以利用 Typescript Service Plugin(例如配置 eslint 規則)阻塞編譯或者在編譯時告警嗎?

閱讀本文需要具備的基礎知識。

預備知識

本文的定位為了解進階用法,故不會涉及過多基礎知識相關的講解,需要讀者自己去完善這方面的知識儲備。

此文檔的内容預設要求讀者已經具備以下知識:

  1. Javascript

    或其他語言程式設計經驗。
  2. Typescript

    實際使用經驗,最好在正經項目中完整地使用過。
  3. 了解

    Typescript

    基礎文法以及常見關鍵字地作用。
  4. Typescript

    類型系統

    架構有一個最基本的了解。

相關資源推薦

  1. Typescript 官網[1]
  2. TypeScript Deep Dive[2]
  3. TypeScript GitHub位址[3]

背景

初用

Typescript

開發的同學一定有這樣的困擾:

  1. 代碼代碼提示并不智能,似乎隻能顯式的定義類型,才能有代碼提示,無法了解這樣的程式設計語言居然有這麼多人趨之若鹜。
  2. 各種各樣的類型報錯苦不堪言,本以為聽信網上說

    Typescript

    可以提高代碼可維護性,結果卻發現徒增了不少開發負擔。
  3. 顯式地定義所有的類型似乎能應付大部分常見,但遇到有些複雜的情況卻發現無能為力,隻能含恨寫下若幹的

    as any

    默默等待代碼

    review

    時的公開處刑。
  4. 項目急時間緊卻發現

    Typescript

    成了首要難題,思索片刻決定投靠的

    Anyscript

    ,快速開發業務邏輯,待到春暖花開時再回來補充類型。雙倍的工作量,雙倍的快樂隻有自己才懂。

為了避免以上悲劇的發生或者重演,我們隻有在對它有更加深刻的了解之後,才能在開發時遊刃有餘、在撸碼時縱橫捭阖。

「思考題」:有人說

Typescript

=

Type

 +

Javascript

,那麼抛開

Javascript

不談,這裡的

Type

是一門完備的程式設計語言嗎?

有過程式設計經驗的同學都知道,函數是一門程式設計語言中最基礎的功能之一,函數是過程化、面向對象、函數式程式設計中程式封裝的基本單元,其重要程度不言而喻。

函數可以幫助我們做很多事,比如 :

  • 函數可以把程式封裝成一個個功能,并形成函數内部的變量作用域,通過靜态變量儲存函數狀态,通過傳回值傳回結果。
  • 函數可以幫助我們實作過程的複用,如果一段邏輯可以被使用多次,就封裝成函數,被其它過程多次調用。
  • 函數也可以幫我們更好地組織代碼結構,幫助我們更好地維護代碼。
那麼言歸正傳,如何在 Typescript 類型系統中定義函數呢?

Typescript

中類型系統中的的函數被稱作  

泛型操作符

,其定義的簡單的方式就是使用

type

關鍵字:

// 這裡我們就定義了一個最簡單的泛型操作符
type foo<T> = T;
           

這裡的代碼如何了解呢,其實這裡我把代碼轉換成大家最熟悉的

Javascript

代碼其實就不難了解了:

// 把上面的類型代碼轉換成 `JavaScript` 代碼
function foo(T) {
  return T
}
           

那麼看到這裡有同學心裡要犯嘀咕了,心想你這不是忽悠我嘛?這不就是  

Typescript

中定義類型的方式嘛?這玩意兒我可太熟了,這玩意兒不就和

interface

一樣的嘛,我還知道

Type

關鍵字和

interface

關鍵字有啥細微的差別呢!

嗯,同學你說的太對了,不過你不要着急,接着聽我說,其實類型系統中的函數還支援對入參的限制。

// 這裡我們就對入參 T 進行了類型限制
type foo<T extends string> = T;
           

那麼把這裡的代碼轉換成我們常見的

Typescript

 是什麼樣子的呢?

function foo(T: string) {
  return T
}
           

當然啦我們也可以給它設定預設值:

// 這裡我們就對入參 T 增加了預設值
type foo<T extends string = 'hello world'> = T;
           

那麼這裡的代碼轉換成我們常見的

Typescript

 就是這樣的:

function foo(T: string = 'hello world') {
  return T
}
           

看到這裡肯定有同學迫不及待地想要提問了:「那能不能像 JS 裡的函數一樣支援剩餘參數呢?」

很遺憾,目前暫時是不支援的,但是在我們日常開發中一定是有這樣的需求存在的。那就真的沒有辦法了嘛?其實也不一定,我們可以通過一些騷操作來模拟這種場景,當然這個是後話了,這裡就不作拓展了。

人生總會面臨很多選擇,程式設計也是一樣。

——我瞎編的

條件判斷也是程式設計語言中最基礎的功能之一,也是我們日常撸碼過程成最常用的功能,無論是

if else

還是

三元運算符

,相信大家都有使用過。

那麼在 Typescript 類型系統中的類型判斷要怎麼實作呢?

其實這在

Typescript

官方文檔被稱為

條件類型(Conditional Types)

,定義的方法也非常簡單,就是使用

extends

關鍵字。

T extends U ? X : Y;
           

這裡相信聰明的你一眼就看出來了,這不就是

三元運算符

嘛!是的,而且這和三元運算符的也發也非常像,如果

T extends U

true

那麼 傳回

X

,否則傳回

Y

結合之前剛剛講過的 "函數",我們就可以簡單的拓展一下:

type num = 1;
type str = 'hello world';

type IsNumber<N> = N extends number ? 'yes, is a number' : 'no, not a number';

type result1 = IsNumber<num>; // "yes, is a number"
type result2 = IsNumber<str>; // "no, not a number"
           

這裡我們就實作了一個簡單的帶判斷邏輯的函數。

模拟真實數組

看到這裡肯定有同學就笑了,這還不簡單,就舉例來說,

Typescript

中最常見資料類型就是

數組(Array)

或者

元組(tuple)

同學你說的很對,「那你知道如何對

元組類型

push

pop

shift

unshift

這些行為操作嗎?」

其實這些操作都是可以被實作的:

// 這裡定義一個工具類型,簡化代碼
type ReplaceValByOwnKey<T, S extends any> = { [P in keyof T]: S[P] };

// shift action
type ShiftAction<T extends any[]> = ((...args: T) => any) extends ((arg1: any, ...rest: infer R) => any) ? R : never;

// unshift action
type UnshiftAction<T extends any[], A> = ((args1: A, ...rest: T) => any) extends ((...args: infer R) => any) ? R : never;

// pop action
type PopAction<T extends any[]> = ReplaceValByOwnKey<ShiftAction<T>, T>;

// push action
type PushAction<T extends any[], E> = ReplaceValByOwnKey<UnshiftAction<T, any>, T & { [k: string]: E }>;

// test ...
type tuple = ['vue', 'react', 'angular'];

type resultWithShiftAction = ShiftAction<tuple>; // ["react", "angular"]
type resultWithUnshiftAction = UnshiftAction<tuple, 'jquery'>; // ["jquery", "vue", "react", "angular"]
type resultWithPopAction = PopAction<tuple>; // ["vue", "react"]
type resultWithPushAction = PushAction<tuple, 'jquery'>; // ["vue", "react", "angular", "jquery"]
           
「注意」:這裡的代碼僅用于測試,操作某些複雜類型可能會報錯,需要做進一步相容處理,這裡簡化了相關代碼,請勿用于生産環境!

相信讀到這裡,大部分同學應該可以已經可以感受到

Typescript

類型系統的強大之處了,其實這裡還是繼續完善,為元組增加

concat

map

等數組的常用的功能,這裡不作詳細探讨,留給同學們自己課後嘗試吧。

但是其實上面提到的 "資料類型" 并不是我這裡想講解的 "資料類型",上述的資料類型本質上還是服務于代碼邏輯的資料類型,其實并不是服務于

類型系統

本身的資料類型。

上面這句話的怎麼了解呢?

不管是

數組

元組

,在廣義的了解中,其實都是用來對 「資料」 作 「批量操作」,同理,服務于

類型系統

本身的資料結構,應該也可以對 「類型」 作 「批量操作」。

那麼如何對 「類型」 作 「批量操作」 呢?或者說服務于

類型系統

中的 「數組」 是什麼呢?

下面就引出了本小節真正的 "數組":

聯合類型(Union Types)

說起

聯合類型(Union Types)

,相信使用過

Typescript

同學的一定對它又愛又恨:

  1. 定義函數入參的時候,當同一個位置的參數允許傳入多種參數類型,使用

    聯合類型(Union Types)

    會非常的友善,但想智能地推導出傳回值的類型地時候卻又犯了難。
  2. 當函數入參個數不确定地時候,又不願意寫出

    (...args: any[]) => void

    這種毫無卵用的參數類型定義。
  3. 使用

    聯合類型(Union Types)

    時,雖然有

    類型守衛(Type guard)

    ,但是某些場景下依然不夠好用。

其實當你對它有足夠的了解時,你就會發現

聯合類型(Union Types)

交叉類型(Intersection Types)

不知道高到哪裡去了,我和它談笑風生。

類型系統中的 "數組"

下面就讓我們更加深入地了解一下 聯合類型(Union Types):

如何周遊 聯合類型(Union Types) 呢?

既然目标是 「批量操作類型」,自然少不了類型的 「周遊」,和大多數程式設計語言方法一樣,在

Typescript

類型系統中也是

in

關鍵字來周遊。

type key = 'vue' | 'react';

type MappedType = { [k in key]: string } // { vue: string; react: string; }
           

你看,通過  

in

關鍵字,我們可以很容易地周遊

聯合類型(Union Types)

,并對類型作一些變換操作。

但有時候并不是所有所有

聯合類型(Union Types)

都是我們顯式地定義出來的。

我們想動态地推導出 聯合類型(Union Types) 類型有哪些方法呢?

可以使用

keyof

關鍵字動态地取出某個鍵值對類型的

key

interface Student {
  name: string;
  age: number;
}

type studentKey = keyof Student; // "name" | "age"
           

同樣的我們也可以通過一些方法取出

元組類型

子類型

type framework = ['vue', 'react', 'angular'];

type frameworkVal1 = framework[number]; // "vue" | "react" | "angular"
type frameworkVal2 = framework[any]; // "vue" | "react" | "angular"
           

實戰應用

看到這裡,有的同學可能要問了,你既然說

聯合類型(Union Types)

可以批量操作類型,「那我想把某一組類型批量映射成另一種類型,該怎麼操作呢」?

方法其實有很多,這裡提供一種思路,抛磚引玉一下,别的方法就留給同學們自行研究吧。

其實分析一下上面那個需求,不難看出,這個需求其實和數組的

map

方法有點相似

那麼如何實作一個操作 聯合類型(Union Types) 的 map 函數呢?

// 這裡的 placeholder 可以鍵入任何你所希望映射成為的類型
type UnionTypesMap<T> = T extends any ? 'placeholder' : never;
           

其實這裡聰明的同學已經看出來,我們隻是利用了

條件類型(Conditional Types)

,使其的判斷條件總是為

true

,那麼它就總是會傳回左邊的類型,我們就可以拿到

泛型操作符

的入參并自定義我們的操作。

讓我們趁熱打鐵,再舉個具體的栗子:把 「聯合類型(Union Types)」 的每一項映射成某個函數的 「傳回值」。

type UnionTypesMap2Func<T> = T extends any ? () => T : never;

type myUnionTypes = "vue" | "react" | "angular";

type myUnionTypes2FuncResult = UnionTypesMap2Func<myUnionTypes>;
// (() => "vue") | (() => "react") | (() => "angular")
           

相信有了上述内容的學習,我們已經對

聯合類型(Union Types)

有了一個相對全面的了解,後續在此基礎之上在作一些進階的拓展,也如砍瓜切菜一般簡單了。

其他資料類型

當然除了數組,還存在其他的資料類型,例如可以用

type

interface

模拟

Javascript

中的 「字面量對象」,其特征之一就是可以使用

myType['propKey']

這樣的方式取出子類型。這裡抛磚引玉一下,有興趣的同學可以自行研究。

Typescript 的類型是支援  "作用域" 的

全局作用域

就像常見的程式設計語言一樣,在

Typescript

的類型系統中,也是支援 「全局作用域」 的。換句話說,你可以在沒有 「導入」 的前提下,在 「任意檔案任意位置」 直接擷取到并且使用它。

通常使用

declare

關鍵字來修飾,例如我們常見的

圖檔資源

的類型定義:

declare module '*.png';
declare module '*.svg';
declare module '*.jpg';
           

當然我們也可以在 「全局作用域」 内聲明一個類型:

declare type str = string;
declare interface Foo {
  propA: string;
  propB: number;
}
           

需要注意的是,如何你的子產品使用了

export

關鍵字導出了内容,上述的聲明方式可能會失效,如果你依然想要将類型聲明到全局,那麼你就需要顯式地聲明到全局:

declare global {
  const ModuleGlobalFoo: string;
}
           

子產品作用域

就像

nodejs

中的子產品一樣,每個檔案都是一個子產品,每個子產品都是獨立的子產品作用域。這裡子產品作用域觸發的條件之一就是使用

export

關鍵字導出内容。

每一個子產品中定義的内容是無法直接在其他子產品中直接擷取到的,如果有需要的話,可以使用

import

關鍵字按需導入。

泛型操作符作用域&函數作用域

泛型操作符是存在作用域的,還記得這一章的第一節為了友善大家了解,我把泛型操作符類比為函數嗎?既然可以類比為函數,那麼函數所具備的性質,泛型操作符自然也可以具備,是以存在泛型操作符作用域自然也就很好了解了。

這裡定義的兩個同名的

T

并不會互相影響:

type TypeOperator<T> = T;
type TypeOperator2<T> = T;
           

上述是關于泛型操作符作用域的描述,下面我們聊一聊真正的函數作用域:

「類型也可以支援閉包」:

function Foo<T> () {
  return function(param: T) {
    return param;
  }
}

const myFooStr = Foo<string>();
// const myFooStr: (param: string) => string
// 這裡觸發了閉包,類型依然可以被保留
const myFooNum = Foo<number>();
// const myFooNum: (param: number) => number
// 這裡觸發了閉包,類型也會保持互相獨立,互不幹涉
           

Typescript

中的類型也是可以支援遞歸的,遞歸相關的問題比較抽象,這裡還是舉例來講解,同時為了友善大家的了解,我也會像第一節一樣,把類型遞歸的邏輯用

Javascript

文法描述一遍。

首先來讓我們舉個栗子:

假如現在需要把一個任意長度的元組類型中的子類型依次取出,并用

&

拼接并傳回。

這裡解決的方法其實非常非常多,解決的思路也非常非常多,由于這一小節講的是 「遞歸」,是以我們使用遞歸的方式來解決。廢話不羅嗦,先上代碼:

// shift action
type ShiftAction<T extends any[]> = ((...args: T) => any) extends ((arg1: any, ...rest: infer R) => any) ? R : never;

type combineTupleTypeWithTecursion<T extends any[], E = {}> = {
  1: E,
  0: combineTupleTypeWithTecursion<ShiftAction<T>, E & T[0]>
}[T extends [] ? 1 : 0]

type test = [{ a: string }, { b: number }];
type testResult = combineTupleTypeWithTecursion<test>; // { a: string; } & { b: number; }
           

看到上面的代碼是不是一臉懵逼?沒關系,接下來我們用普通的

Typescript

代碼來 "翻譯" 一下上述的代碼。

function combineTupleTypeWithTecursion(T: object[], E: object = {}): object {
  return T.length ? combineTupleTypeWithTecursion(T.slice(1), { ...E, ...T[0] }) : E
}

const testData = [{ a: 'hello world' }, { b: 100 }];
// 此時函數的傳回值為 { a: 'hello world', b: 100 }
combineTupleTypeWithTecursion(testData);
           

看到這兒,相信聰明的同學一下子就懂了,原來類型的遞歸與普通函數的遞歸本質上是一樣的。如果觸發結束條件,就直接傳回,否則就一直地遞歸調用下去,所傳遞的第二個參數用來儲存上一次遞歸的計算結果。

當然熟悉遞歸的同學都知道,常見的程式設計語言中,遞歸行為非常消耗計算機資源的,一旦超出了最大限制那麼程式就會崩潰。同理類型中的遞歸也是一樣的,如果遞歸地過深,類型系統一樣會崩潰,是以這裡的代碼大家了解就好,盡量不要在生産環境使用哈。

還記得一開始提出的思考題嗎?其實通過上述的學習,我們完全可以自信地說出,

Typescript

Type

本身也是一套完備的程式設計語言,甚至可以說是完備的圖靈語言。是以類型本身也是可以用來程式設計的,你完全可以用它來編寫一些有趣的東西,更别說是搞定日常開發中遇到的簡單的業務場景了。

其實所謂 "進階用法",不過是用來解決某些特定的場景而産生的特定的約定俗稱的寫法或者文法糖。那進階用法重要嗎?重要,也不重要。怎了解呢,根據程式設計中的 "二八原則",20%的知識儲備已經可以解決80%的需求問題,但是這剩餘的20%,就是入門與熟練的分水嶺。

其實隻要當我們仔細翻閱一遍官方提供的 handbook[4],就已經可以應付日常開發了。但是就像本文一開頭說的那樣,你是否覺得:

  1. Typescript

    在某些場景下用起來很費勁,遠不及

    Javascript

    靈活度的十分之一。
  2. 你是否為自己使用

    Javascript

    中了某些 「騷操作」 用極簡短的代碼解決了某個複雜的代碼而沾沾自喜,但卻為不正确的 「傳回類型」 撓秃了頭。
  3. 你是否明知用了若幹

    as xxx

    會讓你的代碼看起來很挫,但卻無能為力,含恨而終。

同學,當你使用某種辦法解決了上述的這些問題,那麼這種用法就可以被稱作 "進階用法"。

舉個栗子:在

Redux

中有一個叫作

combineReducers

的函數,因為某些場景,我們需要增加一個

combineReducersParamFactory

的函數,該函數支援傳入多個函數,傳入函數的傳回值為作為

combineReducers

的入參,我們需要整合多個入參數函數的傳回值,并生成最終的對象供

combineReducers

函數使用。

思考一下邏輯,發現其實并不複雜,用

Javascript

可以很容易地實作出來:

/**
 * 合并多個參數的傳回數值并傳回
 * @param { Function[] } reducerCreators
 * @returns { Object }
 */
function combineReducersParamFactory(...reducerCreators) {
  return reducerCreators.reduce((acc, creator) => ({ ...acc, ...creator() }), {})
}

// test ...

function todosReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}

function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

const ret = combineReducersParamFactory(
  () => ({ todosReducer }),
  () => ({ counterReducer })
);
// { todosReducer: [Function: todosReducer], counterReducer: [Function: counterReducer] }
           

但如果用需要配備對應的類型,應該如何編寫呢?

type Combine<T> = (T extends any ? (args: T) => any : never) extends (args: infer A) => any ? A : never;

/**
 * 合并多個參數的傳回數值并傳回
 * @param { Function[] } reducerCreators
 * @returns { Object }
 */
function combineReducersParamFactory<T extends ((...args) => object)[]>(...reducerCreators: T): Combine<ReturnType<T[number]>> {
  return reducerCreators.reduce<any>((acc, creator) => ({ ...acc, ...creator() }), {});
}

// test ...

function todosReducer(state: object[], action: { [x: string]: string}) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([action.text])
    default:
      return state
  }
}

function counterReducer(state: number, action: { [x: string]: string}) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// 這裡不需要顯示傳入類型,這裡就可以得到正确的代碼提示
const ret = combineReducersParamFactory(
  () => ({ todosReducer }),
  () => ({ counterReducer })
);
// { todosReducer: [Function: todosReducer], counterReducer: [Function: counterReducer] }
           

你看,類型經過精心編排之後,就是可以讓調用者不增加任何負擔的前提下,享受到代碼提示的快樂。

經過這一章節的學習,我們可以明确了解到,經過我們精心編排的類型,可以變得非常的智能,可以讓調用者幾乎零成本地享受到代碼提示的快樂。或許在編排類型時所耗費的時間成本比較大,但是一旦我們編排完成,就可以極大地減少調用者的腦力負擔,讓調用者享受到程式設計的快樂。

熟悉 「函數式程式設計」 的同學一定對 「資料流動」 的概念有較為深刻的了解。當你在 "上遊" 改變了一個值之後,"下遊" 相關的會跟着自動更新。有 「響應式程式設計」 經驗的同學這是時候應該迫不及待地想舉手了,同學把手放下,這裡我們并不想深入地讨論 「流式程式設計思想」,之是以引出這些概念,是想類比出本小節的重點: 「流動的類型」。

是的,編寫類型系統的思路是可以借鑒 「函數式程式設計」 的思想的。是以某一個類型發生變化時,其他相關的類型也會自動更新,并且當代碼的臃腫到不可維護的時候,你會得到一個友好的提示,整個類型系統就好像一個被精心設計過的限制系統。

聊完了類型系統的編寫思路,咱們再來聊一聊代碼哲學。其實之是以現在

Typescript

越來越火,撇開哪些聊爛了的優勢不談,其實最大的優勢在于強大的類型表現能力,以及編輯器(VSCode)完備的代碼提示能力。

那麼在這些優勢的基礎上,我個人拓展了一些編碼哲學(習慣),這裡見仁見智,大佬輕噴~:

  1. 減少不必要的顯式類型定義,盡可能多地使用類型推導,讓類型的流動像呼吸一樣自然。
  2. 盡可能少地使用

    any

    as any

    ,注意這裡并不是說不能用,而是你判斷出目前情況下使用

    any

    是最優解。
  3. 如果确定要使用

    any

    作為類型,優先考慮一下是否可以使用

    unknown

    類型替代,畢竟

    any

    會破壞類型的流動。
  4. as xxx

    ,如果大量使用這種方式糾正類型,那麼大機率你對 「類型流動」 了解的還不夠透徹。

常見類型推導實作邏輯梳理與實踐入門

前面我們說到,類型是具備流動性的,結合 「響應式程式設計」 的概念其實很容易了解。這一小節我們将列舉幾個常見的例子,來和大家具體講解一下。

有程式設計經驗的同學都知道,資料是可以被傳遞的,同理,類型也可以。

你可用

type

建立一個類型指針,指向對應的類型,那麼就可以實作類型的傳遞,當然你也可以了解為指定起一個别名,或者說是拷貝,這裡見仁見智,但是通過上述方法可以實作類型的傳遞,這是顯而易見的。

type RawType = { a: string, b: number };

// 這裡就拿到了上述類型的引用
type InferType = RawType; // { a: string, b: number };
           

同樣,類型也可以随着資料的傳遞而傳遞:

var num: number = 100;
var num2  = num;

type Num2Type = typeof num2; // number
           

也正是依賴這一點,

Typescript

才得以實作 「類型檢查」、「定義跳轉」 等功能。

到這裡熟悉 「流式程式設計」 的同學就要舉手了:你光說了類型的 「傳遞」,「輸入」  與  「輸出」,那我如果希望在類型 「傳遞」 的過程中對它進行操作,該怎麼做呢?同學你不要急,這正是我下面所想要講的内容。

在上一小節中,我們反複地扯到了 「函數式程式設計」、「響應式程式設計」、「流式程式設計」 這些抽象的概念,其實并不是跑題,而是者兩者的思想(理念)實在太相似了,在本小節後續的講解中,我還會一直延用這些概念幫助大家了解。翻看一下常用 「函數式程式設計」 的庫,不管是

Ramda

RXJS

還是我們耳熟能詳的

lodash

underscore

,裡面一定有一個操作符叫作

filter

,也就是對資料流的過濾。

這個操作符的使用頻率一定遠超其他操作符,那麼這麼重要的功能,我們在類型系統中該如何實作呢?

要解決這個問題,這裡我們先要了解一個在各大 技術社群/平台 搜尋頻率非常高的一個問題:

「TypeScript中 的  never 類型具體有什麼用?」

既然這個問題搜尋頻率非常之高,這裡我也就不重複作答,有興趣的同學可以看一下尤大大的回答: TypeScript中的never類型具體有什麼用? - 尤雨溪的回答 - 知乎[5]。

這裡我們簡單總結一下:

  1. never

    代表空集。
  2. 常用于用于校驗 "類型收窄" 是否符合預期,就是寫出類型絕對安全的代碼。
  3. never

    常被用來作 "類型兜底"。

當然上面的總結并不完整,但已經足夠幫助了解本小節内容,感興趣的同學可以自行查閱相關資料。

上面提到了 "類型收窄",這與我們的目标已經十分接近了,當然我們還需要了解

never

參與類型運算的相關表現:

type NeverTest = string | never // stirng
type NeverTest2 = string & never // never
           

重要的知識出現了:

T | never

,結果為

T

看到這裡,相信聰明的同學們已經有思路了,我們可以用

never

來過濾掉

聯合類型(Union Types)

中不和期望的類型,其實這個 「泛型操作符」 早在 Typescript 2.8[6] 就已經被加入到了官方文檔中了。

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;
           

相信經過這麼長時間的學習,看到這裡你一定很容易就能這種寫法的思路。

好了,講完了 「過濾」,我們再來講講 「分流」。類型 「分流」 的概念其實也不難了解,這個概念常常與邏輯判斷一同出現,畢竟從邏輯層面來講,

聯合類型(Union Types)

本質上還是用來描述 「或」 的關系。同樣的概念如果引入到 「流式程式設計」 中,就自然而然地會引出  「分流」。換成打白話來講,就是不同資料應被分該發到不同的 「管道」 中,同理,類型也需要。

那麼這麼常用的功能,在

Typescript

中如何處理呢?其實這種常見的問題,官方也非常貼心地為我們考慮到了,那就是:

類型守衛(Type guard)

。網上對

類型守衛(Type guard)

有講解的文章非常的多,這裡也不作贅述,有興趣的同學可以自行搜尋學習。我們這裡用一個簡單的栗子簡單地示範一下用法:

function foo(x: A | B) {
  if (x instanceof A) {
    // x is A
  } else {
  // x is B
  }
}
           

「可以觸發類型守衛的常見方式有」:

typeof

instanceof

in

==

===

!=

!==

等等。

當然在有些場景中,單單通過以上的方式不能滿足我們的需求,該怎麼辦呢?其實這種問題,官方也早已經幫我考慮到了:使用

is

關鍵字自定義

類型守衛(Type guard)

// 注意這裡需要傳回 boolean 類型
function isA(x): x is A {
  return true;
}

// 注意這裡需要傳回 boolean 類型
function isB(x): x is B {
  return x instanceof B;
}

function foo2(x: unknown) {
  if (isA(x)) {
    // x is A
  } else {
    // x is B
  }
}
           

這一章節中,我們通過類比

響應式程式設計

流式程式設計

的概念方式,幫助大家更好地了解了 「類型推導」 的實作邏輯與思路,相信經過了這一章節的學習,我們對

Typescript

中的類型推導又有了更加深入的了解。不過這一章引入的抽象的概念比較多,也比較雜,基礎不是太好的同學需要多花點時間翻看一下相關資料。

産生背景

Typescript

的編譯手段大部分同學應該都不會陌生,無論是在

webpack

中使用

ts-loader

babel-loader

,還是在

gulp

gulp-typescript

,亦或是直接使用

Typescript

自帶的指令行工具,相信大部分同學也都已經駕輕就熟了,這裡不做贅述。

這裡我們把目光聚焦到撸碼體驗上,相信有使用過

Typescritp

開發前端項目的同學一定有過各種各樣的困擾,這裡列舉幾個常見的問題:

  1. 在處理 CSS Module 的樣式資源的類型定義時,不滿足于使用

    declare module '*.module.css'

    這種毫無卵用的類型定義。
  2. 不想給編輯器安裝各種各樣的插件,下次啟動編輯器的時間明顯變長,小破電腦不堪重負,而且每次重裝系統都是一次噩夢降臨。
  3. 不想妥協于同僚的使用習慣,想使用自己熟悉的編輯器。
  4. 并不滿足于官方已有的代碼提示,想讓自己的編輯器更加地貼心與智能。

為了提供更加貼心的開發體驗,Typescript 官方提供一種解決思路——Typescript Service Plugins

功能定位

以下内容摘自官方 WIKI:

In TypeScript 2.2 and later, developers can enable language service plugins to 「augment the TypeScript code editing experience」.

其實官方文檔已經寫的很清楚了,這玩意兒旨在優化

Typescript

代碼的 「編寫體驗」。是以想利用這玩意兒改變編譯結果或是想自創新文法的還是省省吧 嗯,我在說我自己呢!

那麼

Typescript Service Plugins

的可以用來做哪些事呢?

官方也有明确的回答:

plugins are for augmenting the editing experience. Some examples of things plugins might do:
  • Provide errors from a linter inline in the editor
  • Filter the completion list to remove certain properties from

    window

  • Redirect "Go to definition" to go to a different location for certain identifiers
  • Enable new errors or completions in string literals for a custom templating language

同樣官方也給出了不推薦使用

Typescript Service Plugins

的場景:

Examples of things language plugins cannot do:
  • Add new custom syntax to TypeScript
  • Change how the compiler emits JavaScript
  • Customize the type system to change what is or isn't an error when running

    tsc

好了,相信讀到這裡大家一定對

Typescript Service Plugins

有了一個大緻的了解,下面我會介紹一下    

Typescript Service Plugins

的安裝與使用。

如何安裝以及如何配置 Typescript Service Plugins

Typescript Service Plugins 的安裝方法
# 就像安裝普通的 `npm` 包一樣
npm install --save-dev your_plugin_name           
如何在 tsconfig.json 中配置 Typescript Service Plugins
{
  "compilerOptions": {
    /** compilerOptions Configuration ... */
    "noImplicitAny": true,
    "plugins": [
      {
        /** 配置插件名稱,也可以填寫本地路徑 */
        "name": "sample-ts-plugin"
        /** 這裡可以給插件傳參 ... */
      }
      /** 支援同時引入多個插件 ... */
    ]
  }
}           
幾個需要注意的地方:
  1. 如果使用

    VSCode

    開發,記得務必 using the workspace version of typescript[7],否則可能導緻插件不生效。
  2. Typescript Service Plugins

    産生的告警或者報錯不會影響編譯結果。
  3. 如果配置完了不生效可以先嘗試重新開機你的編輯器。

具體使用細節請用編輯器打開我提供的 demo,自行體驗。

示例插件:typescript-plugin-css-modules[8]

插件安裝
npm install --save-dev typescript-styled-plugin typescript           
配置方法

在 tsconfig.json 中增加配置

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-styled-plugin"
        /** 具體配置參數請檢視官方文檔 */
      }
    ]
  }
}           
插件基本介紹與使用場景

此插件可以用來緩解在使用

CSS Module

時沒有代碼提示的困境,主要思路就是通過讀取對應的 CSS Module 檔案并解析成對應的  

AST

,并生成對應的類型檔案進而支援對應的代碼提示。但是根據回報來看,似乎某些場景下表現并不盡人意,是否值得大規模使用有待商榷。

類似實作思路的還有 typings-for-css-modules-loader[9],功能來說肯定是

webpack loader

更加強大,但是

Typescript Plugin

更加輕量、入侵度也越低,取舍與否,見仁見智吧

示例插件:typescript-eslint-language-service[10]

npm install --save-dev eslint typescript-eslint-language-service           

.eslintrc.*

檔案中,添加對應的

eslint

配置

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-eslint-language-service"
        /** 預設會讀取 `.eslintrc.*` 檔案 */
        /** 具體配置參數請檢視官方文檔 */
      }
    ]
  }
}           

此插件可以讓

Typescript

原生支援

eslint

檢查及告警,編輯器不需要安裝任何插件即可自持,但是報錯并不影響編譯結果。

示例插件:typescript-styled-plugin[11]

npm install --save-dev typescript-styled-plugin typescript           
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-styled-plugin"
        /** 具體配置參數請檢視官方文檔 */
      }
    ]
  }
}           

此插件可以為 styled-components[12] 的樣式字元串模闆提供

屬性/屬性值

做文法檢查。 同時也推薦安裝

VSCode

插件 vscode-styled-components[13],為你的樣式字元串模闆提供代碼提示以及文法高亮。

  1. Using the Compiler API[14]
  2. Using the Language Service API[15]
  3. Writing a Language Service Plugin[16]
  4. Useful Links for TypeScript Issue Management[17]

「答」:不可以,所有可以使用

Typescript Plugin

的場景一定都是編碼階段的,而且官方對 plugins 的定位局限在了

隻改善編寫體驗

這方面,你并不能自定義文法或者自定義規則來改變編譯結果,不過你可以考慮使用自定義

compiler

,當然這是另一個話題了。

以下引用自官方文檔:

TypeScript Language Service Plugins ("plugins") are for changing the 「editing experience only」. The core TypeScript language remains the same. Plugins can't add new language features such as new syntax or different typechecking behavior, and 「plugins aren't loaded during normal commandline typechecking or emitting」.

Reference

[1]

Typescript 官網: https://www.typescriptlang.org/

[2]

TypeScript Deep Dive: https://basarat.gitbook.io/typescript/

[3]

TypeScript GitHub位址: https://github.com/microsoft/TypeScript

[4]

handbook: https://www.typescriptlang.org/docs/handbook/basic-types.html

[5]

TypeScript中的never類型具體有什麼用? - 尤雨溪的回答 - 知乎: https://www.zhihu.com/question/354601204/answer/888551021

[6]

Typescript 2.8: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#predefined-conditional-types

[7]

using the workspace version of typescript: https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript

[8]

typescript-plugin-css-modules: https://www.npmjs.com/package/typescript-plugin-css-modules

[9]

typings-for-css-modules-loader: https://www.npmjs.com/package/@teamsupercell/typings-for-css-modules-loader

[10]

typescript-eslint-language-service: https://www.npmjs.com/package/typescript-eslint-language-service

[11]

typescript-styled-plugin: https://www.npmjs.com/package/typescript-styled-plugin

[12]

styled-components: https://www.npmjs.com/package/styled-components

[13]

vscode-styled-components: https://github.com/styled-components/vscode-styled-components

[14]

Using the Compiler API: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API

[15]

Using the Language Service API: https://github.com/microsoft/TypeScript/wiki/Using-the-Language-Service-API

[16]

Writing a Language Service Plugin: https://github.com/microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin

[17]

Useful Links for TypeScript Issue Management: https://github.com/microsoft/TypeScript/wiki/Useful-Links-for-TypeScript-Issue-Management

【萬字長文】深入了解 Typescript 進階用法