前言
TypeScript 的官方文件早已更新,但我能找到的中文文件都還停留在比較老的版本。所以對其中新增以及修訂較多的一些章節進行了翻譯整理。
本篇整理自 TypeScript Handbook 中 「Generics」 章節。
本文並不嚴格按照原文翻譯,對部分內容也做了解釋補充。
正文
軟體工程的一個重要部分就是構建元件,元件不僅需要有定義良好和一致的 API,也需要是可複用的(reusable)。好的元件不僅能夠相容今天的資料型別,也能適用於未來可能出現的資料型別,這在構建大型軟體系統時會給你最大的靈活度。
在比如 C# 和 Java 語言中,用來建立可複用元件的工具,我們稱之為泛型(generics)。利用泛型,我們可以建立一個支援眾多型別的元件,這讓使用者可以使用自己的型別消費(consume)這些元件。
Generics 初探(Hello World of Generics)
讓我們開始寫第一個泛型,一個恆等函式(identity function)。所謂恆等函式,就是一個返回任何傳進內容的函式。你也可以把它理解為類似於 echo
命令。
不借助泛型,我們也許需要給予恆等函式一個具體的型別:
function identity(arg: number): number {
return arg;
}
或者,我們使用 any
型別:
function identity(arg: any): any {
return arg;
}
儘管使用 any
型別可以讓我們接受任何型別的 arg
引數,但也讓我們丟失了函式返回時的型別資訊。如果我們傳入一個數字,我們唯一知道的資訊是函式可以返回任何型別的值。
所以我們需要一種可以捕獲引數型別的方式,然後再用它表示返回值的型別。這裡我們用了一個型別變數(type variable),一種用在型別而非值上的特殊的變數。
function identity<Type>(arg: Type): Type {
return arg;
}
現在我們已經給恆等函式加上了一個型別變數 Type
,這個 Type
允許我們捕獲使用者提供的型別,使得我們在接下來可以使用這個型別。這裡,我們再次用 Type
作為返回的值的型別。在現在的寫法裡,我們可以清楚的知道引數和返回值的型別是同一個。
現在這個版本的恆等函式就是一個泛型,它可以支援傳入多種型別。不同於使用 any
,它沒有丟失任何資訊,就跟第一個使用 number
作為引數和返回值型別的的恆等函式一樣準確。
在我們寫了一個泛型恆等函式後,我們有兩種方式可以呼叫它。第一種方式是傳入所有的引數,包括型別引數:
let output = identity<string>("myString"); // let output: string
在這裡,我們使用 <>
而不是 ()
包裹了引數,並明確的設定 Type
為 string
作為函式呼叫的一個引數。
第二種方式可能更常見一些,這裡我們使用了型別引數推斷(type argument inference)(部分中文文件會翻譯為“型別推論”),我們希望編譯器能基於我們傳入的引數自動推斷和設定 Type
的值。
let output = identity("myString"); // let output: string
注意這次我們並沒有用 <>
明確的傳入型別,當編譯器看到 myString
這個值,就會自動設定 Type
為它的型別(即 string
)。
型別引數推斷是一個很有用的工具,它可以讓我們的程式碼更短更易閱讀。而在一些更加複雜的例子中,當編譯器推斷型別失敗,你才需要像上一個例子中那樣,明確的傳入引數。
使用泛型型別變數(Working with Generic Type Variables)
當你建立類似於 identity
這樣的泛型函式時,你會發現,編譯器會強制你在函式體內,正確的使用這些型別引數。這就意味著,你必須認真的對待這些引數,考慮到他們可能是任何一個,甚至是所有的型別(比如用了聯合型別)。
讓我們以 identity
函式為例:
function identity<Type>(arg: Type): Type {
return arg;
}
如果我們想列印 arg
引數的長度呢?我們也許會嘗試這樣寫:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
// Property 'length' does not exist on type 'Type'.
return arg;
}
如果我們這樣做,編譯器會報錯,提示我們正在使用 arg
的 .length
屬性,但是我們卻沒有在其他地方宣告 arg
有這個屬性。我們前面也說了這些型別變數代表了任何甚至所有型別。所以完全有可能,呼叫的時候傳入的是一個 number
型別,但是 number
並沒有 .length
屬性。
現在假設這個函式,使用的是 Type
型別的陣列而不是 Type
。因為我們使用的是陣列,.length
屬性肯定存在。我們就可以像建立其他型別的陣列一樣寫:
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
你可以這樣理解 loggingIdentity
的型別:泛型函式 loggingIdentity
接受一個 Type
型別引數和一個實參 arg
,實參 arg
是一個 Type
型別的陣列。而該函式返回一個 Type
型別的陣列。
如果我們傳入的是一個全是數字型別的陣列,我們的返回值同樣是一個全是數字型別的陣列,因為 Type
會被當成 number
傳入。
現在我們使用型別變數 Type
,是作為我們使用的型別的一部分,而不是之前的一整個型別,這會給我們更大的自由度。
我們也可以這樣寫這個例子,效果是一樣的:
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
泛型型別 (Generic Types)
在上個章節,我們已經建立了一個泛型恆等函式,可以支援傳入不同的型別。在這個章節,我們探索函式本身的型別,以及如何建立泛型介面。
泛型函式的形式就跟其他非泛型函式的一樣,都需要先列一個型別引數列表,這有點像函式宣告:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;
泛型的型別引數可以使用不同的名字,只要數量和使用方式上一致即可:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Input>(arg: Input) => Input = identity;
我們也可以以物件型別的呼叫簽名的形式,書寫這個泛型型別:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;
這可以引導我們寫出第一個泛型介面,讓我們使用上個例子中的物件字面量,然後把它的程式碼移動到介面裡:
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
有的時候,我們會希望將泛型引數作為整個介面的引數,這可以讓我們清楚的知道傳入的是什麼引數 (舉個例子:Dictionary<string>
而不是 Dictionary
)。而且介面裡其他的成員也可以看到。
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
注意在這個例子裡,我們只做了少許改動。不再描述一個泛型函式,而是將一個非泛型函式簽名,作為泛型型別的一部分。
現在當我們使用 GenericIdentityFn
的時候,需要明確給出引數的型別。(在這個例子中,是 number
),有效的鎖定了呼叫簽名使用的型別。
當要描述一個包含泛型的型別時,理解什麼時候把型別引數放在呼叫簽名裡,什麼時候把它放在介面裡是很有用的。
除了泛型介面之外,我們也可以建立泛型類。注意,不可能建立泛型列舉型別和泛型名稱空間。
泛型類(Generic Classes)
泛型類寫法上類似於泛型介面。在類名後面,使用尖括號中 <>
包裹住型別引數列表:
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
在這個例子中,並沒有限制你只能使用 number
型別。我們也可以使用 string
甚至更復雜的型別:
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
就像介面一樣,把型別引數放在類上,可以確保類中的所有屬性都使用了相同的型別。
正如我們在 Class 章節提過的,一個類它的型別有兩部分:靜態部分和例項部分。泛型類僅僅對例項部分生效,所以當我們使用類的時候,注意靜態成員並不能使用型別引數。
泛型約束(Generic Constraints)
在早一點的 loggingIdentity
例子中,我們想要獲取引數 arg
的 .length
屬性,但是編譯器並不能證明每種型別都有 .length
屬性,所以它會提示錯誤:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
// Property 'length' does not exist on type 'Type'.
return arg;
}
相比於能相容任何型別,我們更願意約束這個函式,讓它只能使用帶有 .length
屬性的型別。只要型別有這個成員,我們就允許使用它,但必須至少要有這個成員。為此,我們需要列出對 Type
約束中的必要條件。
為此,我們需要建立一個介面,用來描述約束。這裡,我們建立了一個只有 .length
屬性的介面,然後我們使用這個介面和 extend
關鍵詞實現了約束:
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
現在這個泛型函式被約束了,它不再適用於所有型別:
loggingIdentity(3);
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
我們需要傳入符合約束條件的值:
loggingIdentity({ length: 10, value: 3 });
在泛型約束中使用型別引數(Using Type Parameters in Generic Constraints)
你可以宣告一個型別引數,這個型別引數被其他型別引數約束。
舉個例子,我們希望獲取一個物件給定屬性名的值,為此,我們需要確保我們不會獲取 obj
上不存在的屬性。所以我們在兩個型別之間建立一個約束:
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
在泛型中使用類型別(Using Class Types in Generics)
在 TypeScript 中,當使用工廠模式建立例項的時候,有必要通過他們的建構函式推斷出類的型別,舉個例子:
function create<Type>(c: { new (): Type }): Type {
return new c();
}
下面是一個更復雜的例子,使用原型屬性推斷和約束,建構函式和類例項的關係。
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
TypeScript 系列
如果你對於 TypeScript 有什麼困惑或者其他想要了解的內容,歡迎與我交流,微信:「mqyqingfeng」,公眾號搜尋:「冴羽的JavaScript部落格」或者「yayujs」
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。