一。存在意義
考慮這樣一個場景,identity 函式接受一個引數,並原樣返回:
function identity(arg) {
return arg;
}
從型別上看,無論引數是什麼型別,返回值的型別都與引數一致,藉助 [過載機制](/articles/函式-typescript 筆記 5/#articleHeader9),可以這樣描述:
function identity(arg: number): number;
function identity(arg: string): string;
function identity(arg: boolean): boolean;
// ...等無數個 a => a 的型別描述
過載似乎並不能滿足這個場景,因為我們沒有辦法窮舉 arg 的所有可能型別。既然引數是任意型別,不妨用 any 試試:
function identity(arg: any): any;
覆蓋到了所有型別,卻丟失了引數與返回值的型別對應關係(上面相當於 A => B 的型別對映,而我們想要描述的是 A => A)
泛型與 any
那麼,應該如何表達兩個 any 之間的對應關係呢?
用泛型。這樣描述:
function identity<T>(arg: T): T {
return arg;
}
型別變數 T 與 any 類似,相當於具名 any,這樣就能明確表達 T => T(即 A => A)的型別對映了
二。型別變數
Type variable, a special kind of variable that works on types rather than values.
普通變數代表一個值,而型別變數代表一個型別
從作用上看,變數能夠搬運值,而型別變數搬運的是型別資訊:
This allows us to traffic that type information in one side of the function and out the other.
三。泛型函式
型別變數也叫型別引數,與函式引數類似,區別在於函式引數接受一個具體值,而型別引數接受一個具體型別,例如:
function identity<T>(arg: T): T {
return arg;
}
// 傳參給型別引數
// identity<number>
// 傳參給函式引數(自動推斷型別引數)
identity(1);
// 傳參給函式引數(顯式傳入型別引數)
identity<number>(1);
帶有型別引數的函式稱為泛型函式,其中型別引數代表任意型別(any and all types),所以只有所有型別共有的特徵才能訪問:
function loggingIdentity<T>(arg: T): T {
// 報錯 Property 'length' does not exist on type 'T'.
console.log(arg.length);
return arg;
}
實際上,因為有 void 這個空集在,所以並不存在所有型別通用的屬性或方法。也不能對型別變數做任何假設(比如假定它有 length 屬性),因為它代表一個任意型別,沒有任何約束
除此之外,型別變數 T 就像一個具體型別一樣,可以用於任何具體型別出沒的地方:
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;
}
型別描述
泛型函式的型別描述與普通函式類似:
// 普通函式
let myIdentity: (arg: string) => string =
function(arg: string): string {
return arg;
};
// 泛型函式
let myIdentity: <T>(arg: T) => T =
function<T>(arg: T): T {
return arg;
};
仍然是箭頭函式語法,只是在 (引數列表) 前增加了 <型別引數列表>。同樣的,型別描述中型別引數名也可以與實際的不一致:
let myIdentity: <U>(arg: U) => U =
function<T>(arg: T): T {
return arg;
};
P.S. 特殊的,函式型別描述還可以寫成物件字面量的形式:
// 泛型函式
let myIdentity: { <T>(arg: T): T };
// 普通函式
let myIdentity: { (arg: string): string };
像是介面形式型別描述的退化版本,沒有複用優勢,也不如箭頭函式簡潔,因此,並不常見
四。泛型介面
帶型別引數的介面叫泛型介面,例如可以用介面來描述一個泛型函式:
interface GenericIdentityFn {
<T>(arg: T): T;
}
還有一種非常相像的形式:
interface GenericIdentityFn<T> {
(arg: T): T;
}
這兩種都叫泛型介面,區別在於後者的型別引數 T 作用於整個介面,例如:
interface GenericIdentity<T> {
id(arg: T): T;
idArray(...args: T[]): T[];
}
let id: GenericIdentity<string> = {
id: (s: string) => s,
// 報錯 Types of parameters 's' and 'args' are incompatible.
idArray: (...s: number[]) => s,
};
介面級的型別引數有這種約束作用,成員級的則沒有(僅作用於該泛型成員)
五。泛型類
同樣,帶型別引數的類叫泛型類,例如:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
像介面一樣,泛型類能夠約束該類所有成員關注的目標型別一致:
Putting the type parameter on the class itself lets us make sure all of the properties of the class are working with the same type.
注意,型別引數僅適用於類中的例項成員,靜態成員無法使用型別引數,例如:
class GenericNumber<T> {
// 報錯 Static members cannot reference class type parameters.
static zeroValue: T;
}
因為靜態成員在類例項間共享,無法唯一確定型別引數的具體型別:
let n1: GenericNumber<string>;
// 期望 n1.constructor.zeroValue 是 string
let n2: GenericNumber<number>;
// 期望 n1.constructor.zeroValue 是 number,出現矛盾
P.S. 這一點與 Java 一致,具體見 Static method in a generic class?
六。泛型約束
型別引數太「泛」(any and all)了,在一些場景下,可能想要加以約束,例如:
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;
}
通過介面來描述對型別引數的約束(T extends constraintInterface),比如上面要求型別引數 T 必須具有一個 number 型別的 length 屬性`
另一個典型的場景是工廠方法,例如:
// 要求建構函式 c 必須返回同一類(或子類)的例項
function create<T>(c: {new(): T; }): T {
return new c();
}
此外,還可以在泛型約束中使用型別引數,例如:
function getProperty<T, K extends keyof T>(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'.
能夠用一個型別引數的特徵去約束另一個型別引數,相當強大
七。總結
之所以叫泛型,是因為能夠作用於一系列型別,是在具體型別之上的一層抽象:
Generics are able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.
參考資料
-
[型別引數 | 型別_Haskell 筆記 3](/articles/型別-haskell 筆記 3/#articleHeader9)
暫無評論,快來發表你的看法吧