從 JavaScript 到 TypeScript - 泛型

TypeScript 爲 JavaScriopt 帶來了強類型特性,這就意味着限制了類型的自由度。同一段程序,爲了適應不一樣的類型,就可能須要寫不一樣的處理函數——並且這些處理函數中全部邏輯徹底相同,惟一不一樣的就是類型——這嚴重違反抽象和複用代碼的原則。typescript

一個小實例

咱們來模擬一個場景:某個服務提供了一些不一樣類型的數據,咱們須要先經過一箇中間件對這些數據進行一個基本的處理(好比驗證,容錯等),再對其進行使用。那麼用 JavaScript 來寫應該是這樣的編程

JavaScript 源碼

// 模擬服務,提供不一樣的數據。這裏模擬了一個字符串和一個數值
var service = {
    getStringValue: function() {
        return "a string value";
    },
    getNumberValue: function() {
        return 20;
    }
};

// 處理數據的中間件。這裏用 log 來模擬處理,直接返回數據看成處理後的數據
function middleware(value) {
    console.log(value);
    return value;
}

// JS 中對於類型並不關心,因此這裏沒什麼問題
var sValue = middleware(service.getStringValue());
var nValue = middleware(service.getNumberValue());

改寫成 TypeScript

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

const service = {
    getStringValue(): string {
        return "a string value";
    },

    getNumberValue(): number {
        return 20;
    }
};

爲了保證在對 sValuenValue 的後續操做中類型檢查有效,它們也會有類型(若是 middleware 類型定義得當,能夠推導,這裏咱們先顯示定義其類型)數組

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

如今的問題是 middleware 要怎麼樣定義才既可能返回 string,又可能返回 number,並且還能被類型檢查正確推導出來?dom

第 1 個辦法,用 any

function middleware(value: any): any {
    console.log(value);
    return value;
}

是的,這個辦法能夠檢查經過。但它的問題在於 middleware 內部失去了類型檢查,在後在對 sValuenValue 賦值的時候,也只是看成類型沒有問題。簡單的說,是有「僞裝」沒問題。模塊化

第 2 個辦法,多個 middleware

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

固然也能夠用 TypeScript 的重載(overload)來實現函數

function middleware(value: string): string;
function middleware(value: number): number;
function middleware(value: any): any {
    // 實現同樣沒有嚴格的類型檢查
}

這種方法最主要的一個問題是……若是我有 10 種類型的數據,就須要定義 10 個函數(或重載),那 20 個,200 個呢……工具

正解:使用泛型(Generic)

如今咱們切入正題,用泛型來解決這個問題。那麼這就須要解釋一下什麼是泛型了:泛型就是指定一個表示類型的變量,用它來代替某個實際的類型用於編程,然後經過實際調用時傳入或推導的類型來對其進行替換,以達到一段使用泛型程序能夠實際適應不一樣類型的目的。this

雖然這個解釋已經很接地氣了,可是理解起來仍是不如一個實例來得容易。咱們來看看 middleware 的泛型實現是怎麼樣的spa

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

middleware 後面緊接的 <T> 表示聲明一個表示類型的變量,Value: T 表示聲明參數是 T 類型的,後面的 : T 表示返回值也是 T 類型的。那麼在調用 middlewre(getStringValue()) 的時候,因爲參數推導出來是 string 類型,因此這個時候 T 表明了 string,所以此時 middleware 的返回類型也就是 string;而對於 middleware(getNumberValue()) 調用來講,這裏的 T 表示了 number

咱們直接從 VSCode 的提示能夠看出來,對於 middleware<T>() 調用,TypeScript 能夠推導出參數類型和返回值類型:

clipboard.png

咱們也能夠在調用的時候,小括號前顯示指定 T 代替的類型,好比 mdiddleware<string>(...),不過若是指定的類型與推導的類型有衝突,就會提示錯誤:

clipboard.png

泛型類

前面已經解釋了「泛型」這個概念。示例中泛型的用法咱們稱之爲「泛型函數」。不過泛型更普遍的用法是用於「泛型類」——即在聲明類的時候聲明泛型,那麼在類的整個個做用域範圍內均可以使用聲明的泛型類型。

相信你們都已經對數組有所瞭解,好比 string[] 表示字符串數組類型。其實在早期的 TypeScript 版本中沒有這種數組類型表示,而是採用實例化的泛型 Array<string> 來表示的,如今仍然可使用這方式來表示數組。

除此以外,TypeScript 中還有一個很經常使用的泛型類,Promise<T>。由於 Promise 每每是帶數據的,因此經過 Promise<T> 這種泛型定義的形式,能夠表示一個 Promise 所帶數據的類型。好比下圖就能夠看出,TypeScript 能正確推導出 n 的類型是 number

clipboard.png

因此,泛型類其實多數時候是應用於容器類。假設咱們須要實現一個 FilteredList,咱們能夠向其中 add()(添加) 任意數據,可是它在添加的時候會自動過濾掉不符合條件的一些,最終經過 get all() 輸出全部符合條件的數據(數組)。而過濾條件在構造對象的時候,以函數或 Lambda 表達式提供。

// 聲明泛型類,類型變量爲 T
class FilteredList<T> {
    // 聲明過濾器是以 T 爲參數類型,返回 boolean 的函數表達式
    filter: (v: T) => boolean;
    // 聲明數據是 T 數組類型
    data: T[];
    constructor(filter: (v: T) => boolean) {
        this.filter = filter;
    }

    add(value: T) {
        if (this.filter(value)) {
            this.data.push(value);
        }
    }

    get all(): T[] {
        return this.data;
    }
}

// 處理 string 類型的 FilteredList
const validStrings = new FilteredList<string>(s => !s);

// 處理 number 類型的 FilteredList
const positiveNumber  = new FilteredList<number>(n => n > 0);

甚至還能夠把 (v: T) => boolean 聲明爲一個類型,以便複用

type Predicate<T> = (v: T) => boolean;

class FilteredList<T> {
    filter: Predicate<T>;
    data: T[];
    constructor(filter: Predicate<T>) { ... }
    add(value: T) { ... }
    get all(): T[] { ... }
}

固然類型變量也不必定非得叫 T,也能夠叫 TValue 或別的什麼,可是通常建議以大寫的 T 做爲前綴,採用 Pascal 命名規則,方便識別。還有一些常見的指代,好比 TKey 表示鍵類型,TValue 表示值類型等(經常使用於映射表這類容器定義)。

泛型約束

有了泛型以後,一個函數或容器類能處理的類型一會兒擴到了無限大,彷佛有點失控的感受。因此這裏又產生了一個約束的概念。咱們能夠聲明對類型參數進行約束。

好比,咱們有 IAnimal 這樣一個接口,而後寫一個 run 工具函數,它可讓動物跑起來,並且它會返回這個動物實例自己(以便鏈式調用)。先來定義類型

interface IAnimal {
    run(): void;
}

class Dog implements IAnimal {
    run(): void {
        console.log("Dog is running");
    }
}

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

function run(animal: IAnimal): IAnimal {
    animal.run();
    return animal;
}

const dog = run(new Dog());    // dog: IAnimal

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

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

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

採用這種定義,dog 能夠推導正確。不過因爲 TAnimal 在這裏只是個變量,能夠表明任意類型,因此它並不能保證擁有 run() 方法可供調用。

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

正解是使用泛型約束,將 TAnimal 約束爲實現了 IAnimal。這須要在定義類型變量的使用使用 extends 來約束:

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

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

約束爲類型

有時候咱們但願傳入某個工具方法的參數是一個類型,這樣就能夠經過 new 來生成對象。這在 TypeScript 中一般是使用構造函數來約束的,好比

function create<T extends IAnimal>(type: { new(): T }) {
    return new type();
}

const dog = create(Dog);

這裏約束了 create 能夠建立動物的實例。若是不加 extends IAnimal,那麼這個 create 能夠建立任何類型的實例。

多個類型變量

在使用泛型的時候,固然不會限制只使用一個類型變量,咱們可使用多個,好比能夠這樣定義一個 Pair

class Pair<TKey, TValue> {
    private _key: TKey;
    private _value: TValue;
    constructor(key: TKey, value: TValue) {
        this._key = key;
        this._value = value;
    }

    get key() { return this._key; }
    get value() { return this._value; }
}

其它應用

本身定義泛型結構(泛型類或泛型函數)一般只會在寫比較複雜的應用時發生。可是使用已定義好的泛型是極其常見的,上面已經提到了兩個常見的泛型定義,T[]/Array<T>Promise<T>,除此以外,還有 ES6 的 SetMap 對應於 TypeScript 的泛型定義 Set<T>Map<TK, TV>。另外,泛型還經常使用於 Generator 和 Iterable/Iterator:

// 產生 n 個隨機整數
function* randomInt(n): Iterable<number> {
    for (let i = 0; i < n; i++) {
        yield ~~(Math.random() * Number.MAX_SAFE_INTEGER);
    }
}

for (let n of randomInt(10)) {
    console.log(n);
}

擴展閱讀


關注做者的公衆號「邊城客棧」 →

相關文章
相關標籤/搜索