泛型_TypeScript筆記6
一.存在意義
考慮這樣一個場景,identity
函式接受一個引數,並原樣返回:
function identity(arg) { return arg; }
從型別上看,無論引數是什麼型別,返回值的型別都與引數一致,藉助過載機制,可以這樣描述:
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.