軟件工程中,我們不僅要創(chuàng)建一致的定義良好的API,同時也要考慮可重用性。 組件不僅能夠支持當前的數(shù)據(jù)類型,同時也能支持未來的數(shù)據(jù)類型,這在創(chuàng)建大型系統(tǒng)時為你提供了十分靈活的功能。
在像C#和Java這樣的語言中,可以使用泛型
來創(chuàng)建可重用的組件,一個組件可以支持多種類型的數(shù)據(jù)。
這樣用戶就可以以自己的數(shù)據(jù)類型來使用組件。
下面來創(chuàng)建第一個使用泛型的例子:identity函數(shù)。
這個函數(shù)會返回任何傳入它的值。
你可以把這個函數(shù)當成是echo
命令。
不用泛型的話,這個函數(shù)可能是下面這樣:
function identity(arg: number): number {
return arg;
}
或者,我們使用any
類型來定義函數(shù):
function identity(arg: any): any {
return arg;
}
雖然使用any
類型后這個函數(shù)已經(jīng)能接收任何類型的arg參數(shù),但是卻丟失了一些信息:傳入的類型與返回的類型應(yīng)該是相同的。
如果我們傳入一個數(shù)字,我們只知道任何類型的值都有可能被返回。
因此,我們需要一種方法使用返回值的類型與傳入?yún)?shù)的類型是相同的。 這里,我們使用了類型變量,它是一種特殊的變量,只用于表示類型而不是值。
function identity<T>(arg: T): T {
return arg;
}
我們給identity添加了類型變量T
。
T
幫助我們捕獲用戶傳入的類型(比如:number
),之后我們就可以使用這個類型。
之后我們再次使用了T
當做返回值類型?,F(xiàn)在我們可以知道參數(shù)類型與返回值類型是相同的了。
這允許我們跟蹤函數(shù)里使用的類型的信息。
我們把這個版本的identity
函數(shù)叫做泛型,因為它可以適用于多個類型。
不同于使用any
,它不會丟失信息,像第一個例子那像保持準確性,傳入數(shù)值類型并返回數(shù)值類型。
我們定義了泛型函數(shù)后,可以用兩種方法使用。 第一種是,傳入所有的參數(shù),包含類型參數(shù):
let output = identity<string>("myString"); // type of output will be 'string'
這里我們明確的指定了T
是字符串類型,并做為一個參數(shù)傳給函數(shù),使用了<>
括起來而不是()
。
第二種方法更普遍。利用了類型推論,編譯器會根據(jù)傳入的參數(shù)自動地幫助我們確定T的類型:
let output = identity("myString"); // type of output will be 'string'
注意我們并沒用<>
明確的指定類型,編譯器看到了myString
,把T
設(shè)置為此類型。
類型推論幫助我們保持代碼精簡和高可讀性。如果編譯器不能夠自動地推斷出類型的話,只能像上面那樣明確的傳入T的類型,在一些復(fù)雜的情況下,這是可能出現(xiàn)的。
使用泛型創(chuàng)建像identity
這樣的泛型函數(shù)時,編譯器要求你在函數(shù)體必須正確的使用這個通用的類型。
換句話說,你必須把這些參數(shù)當做是任意或所有類型。
看下之前identity
例子:
function identity<T>(arg: T): T {
return arg;
}
如果我們想同時打印出arg
的長度。
我們很可能會這樣做:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
如果這么做,編譯器會報錯說我們使用了arg
的.length
屬性,但是沒有地方指明arg
具有這個屬性。
記住,這些類型變量代表的是任意類型,所以使用這個函數(shù)的人可能傳入的是個數(shù)字,而數(shù)字是沒有.length
屬性的。
現(xiàn)在假設(shè)我們想操作T
類型的數(shù)組而不直接是T
。由于我們操作的是數(shù)組,所以.length
屬性是應(yīng)該存在的。
我們可以像創(chuàng)建其它數(shù)組一樣創(chuàng)建這個數(shù)組:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
你可以這樣理解loggingIdentity
的類型:泛型函數(shù)loggingIdentity
,接收類型參數(shù)T
,和函數(shù)arg
,它是個元素類型是T
的數(shù)組,并返回元素類型是T
的數(shù)組。
如果我們傳入數(shù)字數(shù)組,將返回一個數(shù)字數(shù)組,因為此時T
的的類型為number
。
這可以讓我們把泛型變量T當做類型的一部分使用,而不是整個類型,增加了靈活性。
我們也可以這樣實現(xiàn)上面的例子:
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
使用過其它語言的話,你可能對這種語法已經(jīng)很熟悉了。
在下一節(jié),會介紹如何創(chuàng)建自定義泛型像Array<T>
一樣。
上一節(jié),我們創(chuàng)建了identity通用函數(shù),可以適用于不同的類型。 在這節(jié),我們研究一下函數(shù)本身的類型,以及如何創(chuàng)建泛型接口。
泛型函數(shù)的類型與非泛型函數(shù)的類型沒什么不同,只是有一個類型參數(shù)在最前面,像函數(shù)聲明一樣:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
我們也可以使用不同的泛型參數(shù)名,只要在數(shù)量上和使用方式上能對應(yīng)上就可以。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
我們還可以使用帶有調(diào)用簽名的對象字面量來定義泛型函數(shù):
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;
這引導(dǎo)我們?nèi)懙谝粋€泛型接口了。 我們把上面例子里的對象字面量拿出來做為一個接口:
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
一個相似的例子,我們可能想把泛型參數(shù)當作整個接口的一個參數(shù)。
這樣我們就能清楚的知道使用的具體是哪個泛型類型(比如:Dictionary<string>而不只是Dictionary
)。
這樣接口里的其它成員也能知道這個參數(shù)的類型了。
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
注意,我們的示例做了少許改動。
不再描述泛型函數(shù),而是把非泛型函數(shù)簽名作為泛型類型一部分。
當我們使用GenericIdentityFn
的時候,還得傳入一個類型參數(shù)來指定泛型類型(這里是:number
),鎖定了之后代碼里使用的類型。
對于描述哪部分類型屬于泛型部分來說,理解何時把參數(shù)放在調(diào)用簽名里和何時放在接口上是很有幫助的。
除了泛型接口,我們還可以創(chuàng)建泛型類。 注意,無法創(chuàng)建泛型枚舉和泛型命名空間。
泛型類看上去與泛型接口差不多。
泛型類使用(<>
)括起泛型類型,跟在類名后面。
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; };
GenericNumber
類的使用是十分直觀的,并且你可能已經(jīng)注意到了,沒有什么去限制它只能使用number
類型。
也可以使用字符串或其它更復(fù)雜的類型。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
與接口一樣,直接把泛型類型放在類后面,可以幫助我們確認類的所有屬性都在使用相同的類型。
我們在類那節(jié)說過,類有兩部分:靜態(tài)部分和實例部分。 泛型類指的是實例部分的類型,所以類的靜態(tài)屬性不能使用這個泛型類型。
你應(yīng)該會記得之前的一個例子,我們有時候想操作某類型的一組值,并且我們知道這組值具有什么樣的屬性。
在loggingIdentity
例子中,我們想訪問arg
的length
屬性,但是編譯器并不能證明每種類型都有length
屬性,所以就報錯了。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
相比于操作any所有類型,我們想要限制函數(shù)去處理任意帶有.length
屬性的所有類型。
只要傳入的類型有這個屬性,我們就允許,就是說至少包含這一屬性。
為此,我們需要列出對于T的約束要求。
為此,我們定義一個接口來描述約束條件。
創(chuàng)建一個包含.length
屬性的接口,使用這個接口和extends
關(guān)鍵字還實現(xiàn)約束:
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;
}
現(xiàn)在這個泛型函數(shù)被定義了約束,因此它不再是適用于任意類型:
loggingIdentity(3); // Error, number doesn't have a .length property
我們需要傳入符合約束類型的值,必須包含必須的屬性:
loggingIdentity({length: 10, value: 3});
你可以聲明一個類型參數(shù),且它被另一個類型參數(shù)所約束。比如,
function find<T, U extends Findable<T>>(n: T, s: U) {
// ...
}
find (giraffe, myAnimals);
在TypeScript使用泛型創(chuàng)建工廠函數(shù)時,需要引用構(gòu)造函數(shù)的類類型。比如,
function create<T>(c: {new(): T; }): T {
return new c();
}
一個更高級的例子,使用原型屬性推斷并約束構(gòu)造函數(shù)與類實例的關(guān)系。
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function findKeeper<A extends Animal, K> (a: {new(): A;
prototype: {keeper: K}}): K {
return a.prototype.keeper;
}
findKeeper(Lion).nametag; // typechecks!