一、TypeScript 泛型
軟件工程中,我們不僅要創建一致的定義良好的API,同時也要考慮可重用性。 組件不僅能夠支持當前的數據類型,同時也能支持未來的數據類型,這在創建大型系統時為你提供了十分靈活的功能。在像C#和Java這樣的語言中,可以使用泛型來創建可重用的組件,一個組件可以支持多種類型的數據。 這樣用戶就可以以自己的數據類型來使用組件。
設計泛型的關鍵目的是在成員之間提供有意義的約束,這些成員可以是:類的實例成員、類的方法、函數參數和函數返回值。
泛型(Generics)是允許同一個函數接受不同類型參數的一種模板。相比於使用 any 類型,使用泛型來創建可復用的組件要更好,因為泛型會保留參數類型。
我們需要一種方法使返回值的類型與傳入參數的類型是相同的。 這里,我們使用了 類型變量,它是一種特殊的變量,只用於表示類型而不是值。
function identity<T>(arg: T): T { return arg; }
我們把這個版本的identity函數叫做泛型,因為它可以適用於多個類型。 不同於使用 any,它不會丟失信息,且保持准確性,傳入數值類型並返回數值類型。
我們定義了泛型函數后,可以用兩種方法使用。 第一種是,傳入所有的參數,包含類型參數:
let output = identity<string>("myString"); // type of output will be 'string'
這里我們明確的指定了T是string類型,並做為一個參數傳給函數,使用了<>括起來而不是()。
第二種方法更普遍。利用了類型推論 -- 即編譯器會根據傳入的參數自動地幫助我們確定T的類型:
let output = identity("myString"); // type of output will be 'string'
注意我們沒必要使用尖括號(<>)來明確地傳入類型;編譯器可以查看myString的值,然后把T設置為它的類型。 類型推論幫助我們保持代碼精簡和高可讀性。
2、使用泛型變量
// 我們先看個例子
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // Error: T doesn't have .length
return arg; }
編譯器會報錯說我們使用了arg的.length屬性,但是沒有地方指明arg具有這個屬性。 記住,這些類型變量代表的是任意類型,所以使用這個函數的人可能傳入的是個數字,而數字是沒有 .length屬性的。
現在假設我們想操作T類型的數組而不直接是T。由於我們操作的是數組,所以.length屬性是應該存在的。 我們可以像創建其它數組一樣創建這個數組:
function loggingIdentity<T>(arg: T[]): T[] { console.log(arg.length); // Array has a .length, so no more error
return arg; }
function loggingIdentity<T>(arg: Array<T>): Array<T> { console.log(arg.length); // Array has a .length, so no more error
return arg; }
3、泛型語法
對於剛接觸 TypeScript 泛型的讀者來說,首次看到 <T> 語法會感到陌生。其實它沒有什么特別,就像傳遞參數一樣,我們傳遞了我們想要用於特定函數調用的類型。
參考上面的圖片,當我們調用 identity<Number>(1) ,Number 類型就像參數 1 一樣,它將在出現 T 的任何位置填充該類型。
圖中 <T> 內部的 T 被稱為類型變量,它是我們希望傳遞給 identity 函數的類型占位符,同時它被分配給 value 參數用來代替它的類型:此時 T 充當的是類型,而不是特定的 Number 類型。
其中 T 代表 Type,在定義泛型時通常用作第一個類型變量名稱。但實際上 T 可以用任何有效名稱代替。除了 T 之外,以下是常見泛型變量代表的意思:
- K(Key):表示對象中的鍵類型;
- V(Value):表示對象中的值類型;
- E(Element):表示元素類型。
其實並不是只能定義一個類型變量,我們可以引入希望定義的任何數量的類型變量。比如我們引入一個新的類型變量 U,用於擴展我們定義的 identity 函數:
function identity <T, U>(value: T, message: U) : T { console.log(message); return value; } console.log(identity<Number, string>(68, "***"));

4、泛型接口 與 泛型類
泛型類看上去與泛型接口差不多。 泛型類使用( <>)括起泛型類型,跟在類名后面。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; };
5、泛型約束:我們有時候想操作某類型的一組值,並且我們知道這組值具有什么樣的屬性。
在 loggingIdentity例子中,我們想訪問arg的length屬性,但是編譯器並不能證明每種類型都有length屬性,所以就報錯了。
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // Error: T doesn't have .length
return arg; }
相比於操作any所有類型,我們想要限制函數去處理任意帶有.length屬性的所有類型。 只要傳入的類型有這個屬性,我們就允許,就是說至少包含這一屬性。 為此,我們需要列出對於T的約束要求。
為此,我們定義一個接口來描述約束條件。 創建一個包含 .length屬性的接口,使用這個接口和extends關鍵字來實現約束:
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); // Now we know it has a .length property, so no more error
return arg; }
// 現在這個泛型函數被定義了約束,因此它不再是適用於任意類型:
loggingIdentity(3); // Error, number doesn't have a .length property // 我們需要傳入符合約束類型的值,必須包含必須的屬性:
loggingIdentity({length: 10, value: 3});
(1)在泛型約束中使用類型參數
你可以聲明一個類型參數,且它被另一個類型參數所約束。 比如,現在我們想要用屬性名從對象里獲取這個屬性。 並且我們想要確保這個屬性存在於對象 obj上,因此我們需要在這兩個類型之間使用約束。
function getProperty(obj: T, key: K) { return obj[key]; } let x = { a: 1, b: 2, c: 3, d: 4 }; getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
(2)在泛型里使用類類型
在TypeScript使用泛型創建工廠函數時,需要引用構造函數的類類型。比如:
function create<T>(c: {new(): T; }): T { return new c(); }
