TypeScript 爲 JavaScriopt 帶來了強類型特性,這就意味着限制了類型的自由度。同一段程序,爲了適應不一樣的類型,就可能須要寫不一樣的處理函數——並且這些處理函數中全部邏輯徹底相同,惟一不一樣的就是類型——這嚴重違反抽象和複用代碼的原則。typescript
咱們來模擬一個場景:某個服務提供了一些不一樣類型的數據,咱們須要先經過一箇中間件對這些數據進行一個基本的處理(好比驗證,容錯等),再對其進行使用。那麼用 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 版的服務有返回類型:segmentfault
const service = { getStringValue(): string { return "a string value"; }, getNumberValue(): number { return 20; } };
爲了保證在對 sValue
和 nValue
的後續操做中類型檢查有效,它們也會有類型(若是 middleware
類型定義得當,能夠推導,這裏咱們先顯示定義其類型)數組
const sValue: string = middleware(service.getStringValue()); const nValue: number = middleware(service.getNumberValue());
如今的問題是 middleware
要怎麼樣定義才既可能返回 string
,又可能返回 number
,並且還能被類型檢查正確推導出來?dom
any
function middleware(value: any): any { console.log(value); return value; }
是的,這個辦法能夠檢查經過。但它的問題在於 middleware
內部失去了類型檢查,在後在對 sValue
和 nValue
賦值的時候,也只是看成類型沒有問題。簡單的說,是有「僞裝」沒問題。模塊化
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 個呢……工具
如今咱們切入正題,用泛型來解決這個問題。那麼這就須要解釋一下什麼是泛型了:泛型就是指定一個表示類型的變量,用它來代替某個實際的類型用於編程,然後經過實際調用時傳入或推導的類型來對其進行替換,以達到一段使用泛型程序能夠實際適應不一樣類型的目的。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 能夠推導出參數類型和返回值類型:
咱們也能夠在調用的時候,小括號前顯示指定 T
代替的類型,好比 mdiddleware<string>(...)
,不過若是指定的類型與推導的類型有衝突,就會提示錯誤:
前面已經解釋了「泛型」這個概念。示例中泛型的用法咱們稱之爲「泛型函數」。不過泛型更普遍的用法是用於「泛型類」——即在聲明類的時候聲明泛型,那麼在類的整個個做用域範圍內均可以使用聲明的泛型類型。
相信你們都已經對數組有所瞭解,好比 string[]
表示字符串數組類型。其實在早期的 TypeScript 版本中沒有這種數組類型表示,而是採用實例化的泛型 Array<string>
來表示的,如今仍然可使用這方式來表示數組。
除此以外,TypeScript 中還有一個很經常使用的泛型類,Promise<T>
。由於 Promise 每每是帶數據的,因此經過 Promise<T>
這種泛型定義的形式,能夠表示一個 Promise 所帶數據的類型。好比下圖就能夠看出,TypeScript 能正確推導出 n
的類型是 number
:
因此,泛型類其實多數時候是應用於容器類。假設咱們須要實現一個 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"); } }
function run(animal: IAnimal): IAnimal { animal.run(); return animal; } const dog = run(new Dog()); // dog: IAnimal
這種定義的缺點是 dog 被推導成 IAnimal
類型,固然能夠經過強制聲明爲 const dog: Dog
來指定其類型,可是誰知道 run()
返回的是 Dog
而不是 Cat
呢。
function run<TAnimal>(animal: TAnimal): TAnimal { animal.run(); // 'run' does not exist on type 'TAnimal' return animal; }
採用這種定義,dog 能夠推導正確。不過因爲 TAnimal
在這裏只是個變量,能夠表明任意類型,因此它並不能保證擁有 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 的 Set
和 Map
對應於 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); }
關注做者的公衆號「邊城客棧」 →