TypeScript筆記 :泛型(六)
function identity<T>(arg: T): T {
return arg;
}
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; }
泛型型別
泛型函式的型別與非泛型函式的型別沒什麼不同,只是有一個型別引數在最前面,像函式宣告一樣:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
我們也可以使用不同的泛型引數名,只要在數量上和使用方式上能對應上就可以。
function identity<T>(arg: T): T { return arg; } let myIdentity: <U>(arg: U) => U = identity;
我們還可以使用帶有呼叫簽名的物件字面量來定義泛型函式:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;
這引導我們去寫第一個泛型介面了。 我們把上面例子裡的物件字面量拿出來做為一個介面:
interface GenericIdentityFn { <T>(arg: T): T; } function identity<T>(arg: T): T { return arg; } let myIdentity: GenericIdentityFn = identity;
一個相似的例子,我們可能想把泛型引數當作整個介面的一個引數。 這樣我們就能清楚的知道使用的具體是哪個泛型型別(比如: Dictionary<string>而不只是Dictionary
)。 這樣接口裡的其它成員也能知道這個引數的型別了。
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
注意,我們的示例做了少許改動。 不再描述泛型函式,而是把非泛型函式簽名作為泛型型別一部分。 當我們使用GenericIdentityFn
的時候,還得傳入一個型別引數來指定泛型型別(這裡是:number
),鎖定了之後程式碼裡使用的型別。 對於描述哪部分型別屬於泛型部分來說,理解何時把引數放在呼叫簽名裡和何時放在介面上是很有幫助的。
泛型類
泛型類看上去與泛型介面差不多。 泛型類使用( <>
)括起泛型型別,跟在類名後面。
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
類的使用是十分直觀的,並且你可能已經注意到了,沒有什麼去限制它只能使用number
型別。 也可以使用字串或其它更復雜的型別。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
與介面一樣,直接把泛型型別放在類後面,可以幫助我們確認類的所有屬性都在使用相同的型別。
我們在類那節說過,類有兩部分:靜態部分和例項部分。 泛型類指的是例項部分的型別,所以類的靜態屬性不能使用這個泛型型別。
泛型約束
有時候想操作某型別的一組值,並且我們知道這組值具有什麼樣的屬性。 在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;
}
相比於操作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});
在泛型約束中使用型別引數
你可以宣告一個型別引數,且它被另一個型別引數所約束。 比如,現在我們想要用屬性名從物件裡獲取這個屬性。 並且我們想要確保這個屬性存在於物件 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'.
在泛型裡使用類型別
在TypeScript使用泛型建立工廠函式時,需要引用建構函式的類型別。
function create<T>(c: {new(): T; }): T {
return new c();
}
一個更高階的例子,使用原型屬性推斷並約束建構函式與類例項的關係。
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 createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!