介紹
這里引入官網一段介紹,了解個大概:
軟件工程中,我們不僅要創建一致的定義良好的API,同時也要考慮可重用性。 組件不僅能夠支持當前的數據類型,同時也能支持未來的數據類型,這在創建大型系統時為你提供了十分靈活的功能。
在像C#和Java這樣的語言中,可以使用
泛型
來創建可重用的組件,一個組件可以支持多種類型的數據。 這樣用戶就可以以自己的數據類型來使用組件。
認識泛型的作用
很多時候我們無法准確定義一個類型,它可以是多種類型,這種情況下我們習慣用 any 來指定它的類型,代表它可以是任意類型。any 雖好用,但是它並不是那么安全的,這時候應該更多考慮泛型。
為了理解泛型的作用,舉個例子說明。我們來創建下面這樣的一個函數,傳入什么參數就返回什么參數,這個函數可以看成是一個 echo 命令:
function echoValue(arg: any): any { return arg }
為了不限制傳入的參數類型,所以使用 any 類型。此函數咋一看是沒問題的,但是缺丟失了一些信息,即傳入的類型與返回的類型應該是相同的,使用 any 不能保證這一點。使用 any 不是一個安全的方案,比如我們來改變一下這個函數,返回傳入值的 length :
function echoValue(arg: any): any { return arg.length }
這樣寫不會報任何錯誤,因為 arg 可以是任意值,所以不管做什么操作都是可以的。但如果函數傳入的參數是 number 類型的,顯然它是沒有 length 屬性的,那么執行時程序就會報錯了。例子雖然很牽強,但也能說明問題,any 的不確定性,注定會帶來各種問題,如果動不動就使用 any,那么也失去了使用 typescript 的意義。
現在我們使用泛型的方法來改寫上面例子:
function echoValue<T>(arg: T): T { return arg }
T 是類型變量,它是一種特殊的變量,只用於表示類型而不是值,使用 <> 定義。定義了類型變量之后,你在函數中任何需要指定類型的地方使用 T 都代表這一種類型,這樣也能保證返回值的類型與傳入參數的類型是相同的了。
我們將這個版本的 echoValue 函數稱作“泛型”,因為它適用於多種類型。定義了泛型函數后,有兩種方法調用它,第一種明確指定 T 的類型:
echoValue<string>('hello world')
第二種方法就是直接調用,更普遍。利用了類型推論 -- 即編譯器會根據傳入的參數自動地幫助我們確定 T 的類型:
echoValue('hello world')
當定義泛型時,不符合的操作都會報錯,比如返回傳入值的 length 時:
function echoValue<T>(arg: T): T { return arg.length // error,類型“T”上不存在屬性“length” }
使用泛型變量
需要認識到泛型變量 T 可以是整個類型,也可以是某個類型的一部分,比如:
function echoValue<T>(arg: T[]): T[] { console.log(arg.length) return arg }
定義泛型變量 T,函數參數是各元素為 T 類型的數組類型,返回值是各元素為 T 類型的數組元素。
T 並不是固定的,你可以寫成 A、B或者其他名字,而且可以在一個函數中定義多個泛型變量,如下面這個例子:
function getArray<T,U>(arg1: T, arg2: U): [T,U]{ return [arg1, arg2] }
我們定義了 T 和 U 兩個泛型變量,第一個參數指定 T 類型,第二個參數指定 U 類型,函數返回一個元組包含類型 T 和 U。
泛型類型
我們可以定義一個泛型函數類型,泛型函數的類型與非泛型函數的類型沒什么不同,只是有一個類型參數在最前面。
直接定義:
let echoValue: <T>(arg: T) => T = function<T>(arg: T): T { return arg }
使用類型別名定義:
type EchoValue = <T>(arg: T) => T let echoValue: EchoValue = function<T>(arg: T): T { return arg }
使用接口定義:
interface EchoValue{ <T>(arg: T): T } let echoValue: EchoValue = function<T>(arg: T): T { return arg } // 可以使用不同的泛型參數名,只要在數量上和使用方式上能對應上就可以 let echoValue2: EchoValue = function<U>(arg: U): U { return arg }
對於接口而言,我們可以把泛型參數當作整個接口的一個參數,這樣我們就能清楚的知道使用的具體是哪個泛型類型。如下:
// 泛型變量作為接口的變量 interface EchoValue<T>{ (arg: T): T } let echoValue: EchoValue<string> = function<T>(arg: T): T { return arg } echoValue(123) // error,類型“123”的參數不能賦給類型“string”的參數 let echoValue2: EchoValue<number> = function<U>(arg: U): U { return arg } echoValue2(123)
泛型類
泛型類看上去與泛型接口差不多。 泛型類使用( <>
)括起泛型類型,跟在類名后面。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } // T 為 number 類型 let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; }; // T 為 string 類型 let stringNumeric = new GenericNumber<string>(); stringNumeric.zeroValue = ""; stringNumeric.add = function(x, y) { return x + y; };
類有兩部分:靜態部分和實例部分, 泛型類指的是實例部分的類型,所以類的靜態屬性不能使用這個泛型類型。
泛型約束
我們有時在操作某值的屬性時,是事先知道它具有此屬性的,但是編譯器不知道,就如上面有個例子,我們訪問 arg.length 是行不通的:
function echoValue<T>(arg: T): T { console.log(arg.length) // 類型“T”上不存在屬性“length” return arg }
現在我們可以通過泛型約束來對泛型變量進行約束,讓它至少包含 length 這一屬性,具體實現如下:
// 定義接口,接口規定必須有 length 這一屬性 interface Lengthwise{ length: number } // 使用接口和 extends 關鍵字實現約束,此時 T 類型就必須包含 length 這一屬性 function echoValue<T extends Lengthwise>(arg: T): T { console.log(arg.length) // 通過,因為被約束的 T 類型是包含 length 屬性的 return arg }
現在這個泛型函數被定義了約束,因此它不再是適用於任意類型:
echoValue(3) // 類型“3”的參數不能賦給類型“Lengthwise”的參數 echoValue({value: 3, length:10}) // right echoValue([1, 2, 3]) // right
泛型約束中使用類型參數
當我們定義一個對象,想對它做一個要求,即只能訪問對象上存在的屬性,該怎么做?來看看這個需求的樣子:
const getProps = (obj, propName) => { return obj[propName] } const o = {a: 'aa', b: 'bb'} getProps(o, 'c') // undefined
const getProps = <T, K extends keyof T>(obj: T, propName: K) => { return obj[propName] } const o = {a: 'aa', b: 'bb'} getProps(o, 'c') // error,類型“"c"”的參數不能賦給類型“"a" | "b"”的參數