從 JavaScript 到 TypeScript - 泛型

2018-08-28 20:34 更新

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

一個小實例

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

JavaScript 源碼

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


// 處理數(shù)據(jù)的中間件。這里用 log 來模擬處理,直接返回數(shù)據(jù)當(dāng)作處理后的數(shù)據(jù)
function middleware(value) {
    console.log(value);
    return value;
}


// JS 中對于類型并不關(guān)心,所以這里沒什么問題
var sValue = middleware(service.getStringValue());
var nValue = middleware(service.getNumberValue());

改寫成 TypeScript

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

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


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

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

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

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

第 1 個辦法,用 any

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

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

第 2 個辦法,多個 middleware

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

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

function middleware(value: string): string;
function middleware(value: number): number;
function middleware(value: any): any {
    // 實現(xiàn)一樣沒有嚴(yán)格的類型檢查
}

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

正解:使用泛型(Generic)

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

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

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

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

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

clipboard.png

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

clipboard.png

泛型類

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

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

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

clipboard.png

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

// 聲明泛型類,類型變量為 T
class FilteredList<T> {
    // 聲明過濾器是以 T 為參數(shù)類型,返回 boolean 的函數(shù)表達(dá)式
    filter: (v: T) => boolean;
    // 聲明數(shù)據(jù)是 T 數(shù)組類型
    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 聲明為一個類型,以便復(fù)用

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


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

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

泛型約束

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

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

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 被推導(dǎo)成 IAnimal 類型,當(dāng)然可以通過強(qiáng)制聲明為 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 可以推導(dǎo)正確。不過由于 TAnimal 在這里只是個變量,可以代表任意類型,所以它并不能保證擁有 run() 方法可供調(diào)用。

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

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

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

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

約束為類型

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

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


const dog = create(Dog);

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

多個類型變量

在使用泛型的時候,當(dāng)然不會限制只使用一個類型變量,我們可以使用多個,比如可以這樣定義一個 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; }
}

其它應(yīng)用

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

// 產(chǎn)生 n 個隨機(jī)整數(shù)
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);
}

擴(kuò)展閱讀

此文首發(fā)于 SegmentFault

敬請 掃碼 關(guān)注〔邊城〕的公眾號:邊城客棧

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號