TypeScript Generics(泛型)


軟件工程的一個主要部分就是構建組件,構建的組件不僅需要具有明確的定義和統一的接口,同時也需要組件可復用。支持現有的數據類型和將來添加的數據類型的組件為大型軟件系統的開發過程提供很好的靈活性。

在C#和Java中,可以使用"泛型"來創建可復用的組件,並且組件可支持多種數據類型。這樣便可以讓用戶根據自己的數據類型來使用組件。

泛型的簡單案例

首先,用泛型寫一個"Hello World":identity函數。identity函數將會返回我們傳入的數據。你可以認為它是個"echo"命令。
不用泛型,我們也不用給identity函數指定類型:

function identity(arg: number): number {
    return arg;
}

或者,我們可以給identity函數指定"any"類型:

function identity(arg: any): any {
    return arg;
}

雖然使用"any"類型的時候可以接收任何類型的"arg"參數,但是實際上已經失去函數返回值類型的信息。假如我們傳入一個number,我們只知道返回任何類型的值都是可以的。

所以,我們需要一直方式來捕捉參數的類型,也可以用它來表示返回值的類型。這里使用的是"類型變量",一種特殊的變量,代表的是類型而非值。

function identity<T>(arg: T): T {
    return arg;
}

現在我們已經為identity函數添加了類型變量"T"。"T"允許捕獲用戶提供的參數類型(如:number),以便我們稍后可以使用該類型。然后我們再次用"T"作為返回值的類型。現在我們可以看到,同一類型被用來作為參數類型和返回值類型。

我們稱這個版本的identity函數為泛型,它可用於多種類型。與使用"any"類型不同,它和第一個identity函數(使用number作為參數類型和返回值類型)一樣精准(它不會失去任何信息)。

一旦我們定義了泛型函數,有兩種方法可以使用。第一種就是傳入所有的參數,包括類型參數:

var output = identity<string>("myString"); // output的類型將會是 'string'

在這里,我們明確的將"T"指定為string,作為函數中傳入的參數,使用<>包裹該參數而非()。

第二種是最常見的。我們使用/類型推斷/,我們希望編譯器根據傳入的參數自動為"T"指定類型。

var output = identity("myString"); // output的類型將會是 'string'

注意,我們並未顯示的給尖括號<>內傳入類型,編譯器檢查"myString",然后將"T"設置為它的類型。雖然類型推斷是個很實用的工具,也能夠使代碼簡短易讀,但你還是需要跟前面的例子一樣明確的傳遞類型參數,因為可能會存在復雜的函數,使得編譯器未能正確的進行類型推斷。

使用泛型

當你開始使用泛型,你可能會注意到當你創建一個類似"identity"的泛型函數,編譯器會強制要求你在函數中正確的使用這些通用類型參數。也就是說,你真的把這些參數視為可以是任何類型的。

再看看之前的identity函數:

function identity<T>(arg: T): T {
    return arg;
}

想要每次調用的時候在控制台打印出"arg"參數的length。我們可以嘗試這么寫:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // 錯誤: T 不存在 .length
    return arg;
}

當我們這么做的時候,編譯器會拋出一個錯誤提示我們使用"arg"的".length"屬性,但是沒有地方指定過"arg"的".length"屬性。之前我們說類型變量代表了所有類型,所以可能使用這個函數的時候會傳入一個"number"類型的值,而"number"是沒".length"屬性的。
實際上,我們想讓這個函數接受的參數是個"T"類型的數組而非直接"T"。當傳入的是數組,length屬性便是可用的了。我們可以像創建其他數組類型一樣:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // 數組中存在 .length,所以沒報錯
    return arg;
}

你可以這樣理解loggingIdentity函數:loggingIdentity泛型函數,參數類型是"T",參數"arg"是個類型為"T"的數組,返回的也是個類型為"T"的數組。如果我們傳入一個都是數字的數組,那么我們也會得到一個都是數字的數組,因為這時候"T"類型已經綁定為number了。這使得我們可以使用類型變量"T"作為我們使用的類型的一部分,而非全部類型,這也更具靈活性。

我們可以通過這種方式寫個例子:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // 數組中存在 .length,所以沒報錯
    return arg;
}

泛型類型

在前面例子中,我們創建了通用的identity函數,可以使用於不同的類型。現在,我們將探討函數類型及如何創建泛型接口。

泛型函數的類型與非泛型函數一樣,只是最前面放上一個類型參數,類似與聲明函數:

function identity<T>(arg: T): T {
    return arg;
}

var myIdentity: <T>(arg: T)=>T = identity;

我們也可以給類型中的泛型類型參數指定不同的名稱,只要類型變量的數量和其使用方式都能對應的上。

function identity<T>(arg: T): T {
    return arg;
}

var myIdentity: <U>(arg: U)=>U = identity;

我們也可以使用對象字面量的簽名調用來寫泛型類型:

function identity<T>(arg: T): T {
    return arg;
}

var myIdentity: {<T>(arg: T): T} = identity;

下面開始寫第一個泛型接口。用上個例子中的對象字面量來寫接口:

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

var myIdentity: GenericIdentityFn<number> = identity;
var num = myIdentity(10); // 正確,因為類型是number
var str = myIdentity("10") // 錯誤,參數類型不是number

注意,我們的例子稍微有些改變。我們把非泛型函數簽名作為泛型類型的一部分,而不是去描述泛型函數。當我們使用GenericIdentityFn,我們還需要指定對應的類型參數(這里是number),有效的鎖定在底層簽名調用時會用到的類型。理解"何時將類型參數直接放到簽名調用"和"何時將它放到接口上"將有助於描述哪部分類型屬於泛型。

除了泛型接口,我們還可以創建泛型類。請注意,不可能創建泛型枚舉和模塊。

泛型類

泛型類和泛型接口相似。泛型類在類名后面使用尖括號<>包含泛型類型參數列表。

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

var myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 1;
myGenericNumber.add = function(x, y) { return x + y; };
alert(myGenericNumber.add(myGenericNumber.zeroValue, 1));  // 2

這是對"GenericNumber"類想當直觀的使用,你也可能注意到並未限制只能使用"number"類型。我們可以使用"string"抑或更復雜的對象。

var stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "Hello ";
stringNumeric.add = function(x, y) { return x + y; };

alert(stringNumeric.add(stringNumeric.zeroValue, "World")); // Hello World

和接口一樣,將類型參數放在類之后來告訴我們類的所有屬性都是同一個類型。

正如前面"類"那一節所描述的,一個類由兩部分組成:靜態部分和實例部分。泛型類僅屬於實例部分,所以當我們使用類的時候,靜態成員不能使用類的類型參數。

泛型的限制

如果你還記得之前的例子,有時候你想要寫一個泛型函數來操作一組類型,並且你是知道這些類型具有什么功能。

在"loggingIdentity"例子中,我們希望能夠訪問"arg"的".length"屬性,但是編譯器不能確定每個類型都有".length"屬性,所以它將報錯提示我們不能這么做。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // 錯誤: T 不存在 .length
    return arg;
}

相對於處理任何類型或者所有類型,我們更希望強制去要求函數去處理帶有".length"屬性的任何類型或者所有類型。只要該類型有這個成員(屬性),我們便運行通過,也就是必須包含這個指定的成員(屬性)。

既然需要這么做,我們就創建一個描述限制的接口。在這里,先創建一個只有單個屬性".length"的接口,然后使用這個接口和"extends"關鍵字來指明限制:

interface hasLength {
    length: number;
}

function loggingIdentity<T extends hasLength>(arg: T): T {
    console.log(arg.length);  // 現在我們知道它含有.length屬性,並且不報錯
    return arg;
}

因為這個泛型函數現在是有限制的,所以它不在支持任何類型或者所有類型:

loggingIdentity(3); // 錯誤,number不包含.length屬性

因此,我們需要傳入其類型具有所需屬性的值:

loggingIdentity({length: 10, value: 3}); // 正確

在泛型中使用類類型
當在TypeScript中使用泛型創建工廠函數的時候,需要引用其構造函數的類類型。

class Greeter{
    greeter:string = "Hello World";
}
function create<T>(c: {new(): T}): T { 
    return new c();
}
var newGreeter = create<Greeter>(Greeter);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM