從 JavaScript 到 TypeScript - 泛型

2018-08-28 20:34 更新

TypeScript 為 JavaScriopt 帶來了強類型特性,這就意味著限制了類型的自由度。同一段程序,為了適應不同的類型,就可能需要寫不同的處理函數(shù)——而且這些處理函數(shù)中所有邏輯完全相同,唯一不同的就是類型——這嚴重違反抽象和復用代碼的原則。

一個小實例

我們來模擬一個場景:某個服務提供了一些不同類型的數(shù)據(jù),我們需要先通過一個中間件對這些數(shù)據(jù)進行一個基本的處理(比如驗證,容錯等),再對其進行使用。那么用 JavaScript 來寫應該是這樣的

JavaScript 源碼

  1. // 模擬服務,提供不同的數(shù)據(jù)。這里模擬了一個字符串和一個數(shù)值
  2. var service = {
  3. getStringValue: function() {
  4. return "a string value";
  5. },
  6. getNumberValue: function() {
  7. return 20;
  8. }
  9. };
  10. // 處理數(shù)據(jù)的中間件。這里用 log 來模擬處理,直接返回數(shù)據(jù)當作處理后的數(shù)據(jù)
  11. function middleware(value) {
  12. console.log(value);
  13. return value;
  14. }
  15. // JS 中對于類型并不關心,所以這里沒什么問題
  16. var sValue = middleware(service.getStringValue());
  17. var nValue = middleware(service.getNumberValue());

改寫成 TypeScript

先來看看對服務的改寫,TypeScript 版的服務有返回類型:

  1. const service = {
  2. getStringValue(): string {
  3. return "a string value";
  4. },
  5. getNumberValue(): number {
  6. return 20;
  7. }
  8. };

為了保證在對 sValuenValue 的后續(xù)操作中類型檢查有效,它們也會有類型(如果 middleware 類型定義得當,可以推導,這里我們先顯示定義其類型)

  1. const sValue: string = middleware(service.getStringValue());
  2. const nValue: number = middleware(service.getNumberValue());

現(xiàn)在的問題是 middleware 要怎么樣定義才既可能返回 string,又可能返回 number,而且還能被類型檢查正確推導出來?

第 1 個辦法,用 any

  1. function middleware(value: any): any {
  2. console.log(value);
  3. return value;
  4. }

是的,這個辦法可以檢查通過。但它的問題在于 middleware 內(nèi)部失去了類型檢查,在后在對 sValuenValue 賦值的時候,也只是當作類型沒有問題。簡單的說,是有“假裝”沒問題。

第 2 個辦法,多個 middleware

  1. function middleware1(value: string): string { ... }
  2. function middleware2(value: number): number { ... }

當然也可以用 TypeScript 的重載(overload)來實現(xiàn)

  1. function middleware(value: string): string;
  2. function middleware(value: number): number;
  3. function middleware(value: any): any {
  4. // 實現(xiàn)一樣沒有嚴格的類型檢查
  5. }

這種方法最主要的一個問題是……如果我有 10 種類型的數(shù)據(jù),就需要定義 10 個函數(shù)(或重載),那 20 個,200 個呢……

正解:使用泛型(Generic)

現(xiàn)在我們切入正題,用泛型來解決這個問題。那么這就需要解釋一下什么是泛型了:泛型就是指定一個表示類型的變量,用它來代替某個實際的類型用于編程,而后通過實際調(diào)用時傳入或推導的類型來對其進行替換,以達到一段使用泛型程序可以實際適應不同類型的目的。

雖然這個解釋已經(jīng)很接地氣了,但是理解起來還是不如一個實例來得容易。我們來看看 middleware 的泛型實現(xiàn)是怎么樣的

  1. function middleware<T>(value: T): T {
  2. console.log(value);
  3. return value;
  4. }

middleware 后面緊接的 <T> 表示聲明一個表示類型的變量,Value: T 表示聲明參數(shù)是 T 類型的,后面的 : T 表示返回值也是 T 類型的。那么在調(diào)用 middlewre(getStringValue()) 的時候,由于參數(shù)推導出來是 string 類型,所以這個時候 T 代表了 string,因此此時 middleware 的返回類型也就是 string;而對于 middleware(getNumberValue()) 調(diào)用來說,這里的 T 表示了 number。

我們直接從 VSCode 的提示可以看出來,對于 middleware<T>() 調(diào)用,TypeScript 可以推導出參數(shù)類型和返回值類型:

clipboard.png

我們也可以在調(diào)用的時候,小括號前顯示指定 T 代替的類型,比如 mdiddleware<string>(...),不過如果指定的類型與推導的類型有沖突,就會提示錯誤:

clipboard.png

泛型類

前面已經(jīng)解釋了“泛型”這個概念。示例中泛型的用法我們稱之為“泛型函數(shù)”。不過泛型更廣泛的用法是用于“泛型類”——即在聲明類的時候聲明泛型,那么在類的整個個作用域范圍內(nèi)都可以使用聲明的泛型類型。

相信大家都已經(jīng)對數(shù)組有所了解,比如 string[] 表示字符串數(shù)組類型。其實在早期的 TypeScript 版本中沒有這種數(shù)組類型表示,而是采用實例化的泛型 Array<string> 來表示的,現(xiàn)在仍然可以使用這方式來表示數(shù)組。

除此之外,TypeScript 中還有一個很常用的泛型類,Promise<T>。因為 Promise 往往是帶數(shù)據(jù)的,所以通過 Promise<T> 這種泛型定義的形式,可以表示一個 Promise 所帶數(shù)據(jù)的類型。比如下圖就可以看出,TypeScript 能正確推導出 n 的類型是 number

clipboard.png

所以,泛型類其實多數(shù)時候是應用于容器類。假設我們需要實現(xiàn)一個 FilteredList,我們可以向其中 add()(添加) 任意數(shù)據(jù),但是它在添加的時候會自動過濾掉不符合條件的一些,最終通過 get all() 輸出所有符合條件的數(shù)據(jù)(數(shù)組)。而過濾條件在構造對象的時候,以函數(shù)或 Lambda 表達式提供。

  1. // 聲明泛型類,類型變量為 T
  2. class FilteredList<T> {
  3. // 聲明過濾器是以 T 為參數(shù)類型,返回 boolean 的函數(shù)表達式
  4. filter: (v: T) => boolean;
  5. // 聲明數(shù)據(jù)是 T 數(shù)組類型
  6. data: T[];
  7. constructor(filter: (v: T) => boolean) {
  8. this.filter = filter;
  9. }
  10. add(value: T) {
  11. if (this.filter(value)) {
  12. this.data.push(value);
  13. }
  14. }
  15. get all(): T[] {
  16. return this.data;
  17. }
  18. }
  19. // 處理 string 類型的 FilteredList
  20. const validStrings = new FilteredList<string>(s => !s);
  21. // 處理 number 類型的 FilteredList
  22. const positiveNumber = new FilteredList<number>(n => n > 0);

甚至還可以把 (v: T) => boolean 聲明為一個類型,以便復用

  1. type Predicate<T> = (v: T) => boolean;
  2. class FilteredList<T> {
  3. filter: Predicate<T>;
  4. data: T[];
  5. constructor(filter: Predicate<T>) { ... }
  6. add(value: T) { ... }
  7. get all(): T[] { ... }
  8. }

當然類型變量也不一定非得叫 T,也可以叫 TValue 或別的什么,但是一般建議以大寫的 T 作為前綴,采用 Pascal 命名規(guī)則,方便識別。還有一些常見的指代,比如 TKey 表示鍵類型,TValue 表示值類型等(常用于映射表這類容器定義)。

泛型約束

有了泛型之后,一個函數(shù)或容器類能處理的類型一下子擴到了無限大,似乎有點失控的感覺。所以這里又產(chǎn)生了一個約束的概念。我們可以聲明對類型參數(shù)進行約束。

比如,我們有 IAnimal 這樣一個接口,然后寫一個 run 工具函數(shù),它可以讓動物跑起來,而且它會返回這個動物實例本身(以便鏈式調(diào)用)。先來定義類型

  1. interface IAnimal {
  2. run(): void;
  3. }
  4. class Dog implements IAnimal {
  5. run(): void {
  6. console.log("Dog is running");
  7. }
  8. }

第 1 種 run 定義,使用接口或基類類型

  1. function run(animal: IAnimal): IAnimal {
  2. animal.run();
  3. return animal;
  4. }
  5. const dog = run(new Dog()); // dog: IAnimal

這種定義的缺點是 dog 被推導成 IAnimal 類型,當然可以通過強制聲明為 const dog: Dog 來指定其類型,但是誰知道 run() 返回的是 Dog 而不是 Cat 呢。

第 2 種 run 定義,使用泛型(無約束)

  1. function run<TAnimal>(animal: TAnimal): TAnimal {
  2. animal.run(); // 'run' does not exist on type 'TAnimal'
  3. return animal;
  4. }

采用這種定義,dog 可以推導正確。不過由于 TAnimal 在這里只是個變量,可以代表任意類型,所以它并不能保證擁有 run() 方法可供調(diào)用。

第 3 種 run 定義,使用泛型約束

正解是使用泛型約束,將 TAnimal 約束為實現(xiàn)了 IAnimal。這需要在定義類型變量的使用使用 extends 來約束:

  1. function run<TAnimal extends IAnimal>(animal: TAnimal): TAnimal {
  2. animal.run(); // it's ok
  3. return animal;
  4. }

注意這里的語法,<TAnimal extends IAnimal>,雖然 IAnimal 是個接口,但這里不是在實現(xiàn)接口,extends 表示約束關系,而非繼承。它表示 extends 左邊的類型變量實現(xiàn)了右邊的類型,或者是右邊類型的子孫類,或者就是右邊的那個類型。簡單的說,就是左邊類型的實例可以賦值給右邊類型的變量。

約束為類型

有時候我們希望傳入某個工具方法的參數(shù)是一個類型,這樣就可以通過 new 來生成對象。這在 TypeScript 中通常是使用構造函數(shù)來約束的,比如

  1. function create<T extends IAnimal>(type: { new(): T }) {
  2. return new type();
  3. }
  4. const dog = create(Dog);

這里約束了 create 可以創(chuàng)建動物的實例。如果不加 extends IAnimal,那么這個 create 可以創(chuàng)建任何類型的實例。

多個類型變量

在使用泛型的時候,當然不會限制只使用一個類型變量,我們可以使用多個,比如可以這樣定義一個 Pair

  1. class Pair<TKey, TValue> {
  2. private _key: TKey;
  3. private _value: TValue;
  4. constructor(key: TKey, value: TValue) {
  5. this._key = key;
  6. this._value = value;
  7. }
  8. get key() { return this._key; }
  9. get value() { return this._value; }
  10. }

其它應用

自己定義泛型結構(泛型類或泛型函數(shù))通常只會在寫比較復雜的應用時發(fā)生。但是使用已定義好的泛型是極其常見的,上面已經(jīng)提到了兩個常見的泛型定義,T[]/Array<T>Promise<T>,除此之外,還有 ES6 的 SetMap 對應于 TypeScript 的泛型定義 Set<T>Map<TK, TV>。另外,泛型還常用于 Generator 和 Iterable/Iterator:

  1. // 產(chǎn)生 n 個隨機整數(shù)
  2. function* randomInt(n): Iterable<number> {
  3. for (let i = 0; i < n; i++) {
  4. yield ~~(Math.random() * Number.MAX_SAFE_INTEGER);
  5. }
  6. }
  7. for (let n of randomInt(10)) {
  8. console.log(n);
  9. }

擴展閱讀

此文首發(fā)于 SegmentFault

敬請 掃碼 關注〔邊城〕的公眾號:邊城客棧

公眾號“邊城客?!? /></p></div>
				          <div style=

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號