TypeScript 泛型(Generic)
本節開始介紹 TypeScript 一些進階知識點,第一個要介紹的泛型是 TypeScript 中非常重要的一個概念,它是一種用以增強函數、類和接口能力的非常可靠的手段。
使用泛型,我們可以輕松地將那些輸入重復的代碼,構建為可復用的組件,這給予了開發者創造靈活、可重用代碼的能力。
1. 慕課解釋
泛型在傳統的面向對象語言中極為常見,可以使用泛型來創建可重用的組件,一個組件可以支持多種類型的數據。
通俗來講:泛型是指在定義函數、接口或者類時,未指定其參數類型,只有在運行時傳入才能確定。那么此時的參數類型就是一個變量,通常用大寫字母 T
來表示,當然你也可以使用其他字符,如:U
、K
等。
語法:在函數名、接口名或者類名添加后綴 <T>
:
function generic<T>() {}
interface Generic<T> {}
class Generic<T> {}
2. 初識泛型
之所以使用泛型,是因為它幫助我們為不同類型的輸入,復用相同的代碼。
比如寫一個最簡單的函數,這個函數會返回任何傳入它的值。如果傳入的是 number 類型:
function identity(arg: number): number {
return arg
}
如果傳入的是 string 類型:
function identity(arg: string): string {
return arg
}
通過泛型,可以把兩個函數統一起來:
function identity<T>(arg: T): T {
return arg
}
需要注意的是,泛型函數的返回值類型是根據你的業務需求決定,并非一定要返回泛型類型 T:
function identity<T>(arg: T): string {
return String(arg)
}
代碼解釋: 入參的類型是未知的,但是通過 String 轉換,返回字符串類型。
3. 多個類型參數
泛型函數可以定義多個類型參數:
function extend<T, U>(first: T, second: U): T & U {
for(const key in second) {
(first as T & U)[key] = second[key] as any
}
return first as T & U
}
代碼解釋: 這個函數用來合并兩個對象,具體實現暫且不去管它,這里只需要關注泛型多個類型參數的使用方式,其語法為通過逗號分隔 <T, U, K>
。
4. 泛型參數默認類型
函數參數可以定義默認值,泛型參數同樣可以定義默認類型:
function min<T = number>(arr:T[]): T{
let min = arr[0]
arr.forEach((value)=>{
if(value < min) {
min = value
}
})
return min
}
console.log(min([20, 6, 8n])) // 6
解釋: 同樣的不用去關注這個最小數函數的具體實現,要知道默認參數語法為 <T = 默認類型>
。
5. 泛型類型與泛型接口
先來回顧下之前章節介紹的函數類型:
const add: (x: number, y: number) => string = function(x: number, y: number): string {
return (x + y).toString()
}
等號左側的 (x: number, y: number) => string
為函數類型。
再看下泛型類型:
function identity<T>(arg: T): T {
return arg
}
let myIdentity: <T>(arg: T) => T = identity
同樣的等號左側的 <T>(arg: T) => T
即為泛型類型,它還有另一種帶有調用簽名的對象字面量書寫方式:{ <T>(arg: T): T }
:
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
進一步,把泛型參數當作整個接口的一個參數,我們可以把泛型參數提前到接口名上。這樣我們就能清楚的知道使用的具體是哪個泛型類型:
interface GenericIdentityFn<T> {
(arg: T): T
}
function identity<T>(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn<number> = identity
注意,在使用泛型接口時,需要傳入一個類型參數來指定泛型類型。示例中傳入了 number 類型,這就鎖定了之后代碼里使用的類型。
6. 泛型類
始終要記得,使用泛型是因為可以復用不同類型的代碼。下面用一個最小堆算法舉例說明泛型類的使用:
class MinClass {
public list: number[] = []
add(num: number) {
this.list.push(num)
}
min(): number {
let minNum = this.list[0]
for (let i = 0; i < this.list.length; i++) {
if (minNum > this.list[i]) {
minNum = this.list[i]
}
}
return minNum
}
}
代碼解釋: 示例中我們實現了一個查找 number 類型的最小堆類,但我們的最小堆還需要支持字符串類型,此時就需要泛型的幫助了:
// 類名后加上 <T>
class MinClass<T> {
public list: T[] = []
add(num: T) {
this.list.push(num)
}
min(): T {
let minNum = this.list[0]
for (let i = 0; i < this.list.length; i++) {
if (minNum > this.list[i]) {
minNum = this.list[i]
}
}
return minNum
}
}
let m = new MinClass<string>()
m.add('hello')
m.add('world')
m.add('generic')
console.log(m.min()) // generic
代碼解釋:
第 2 行,在聲明 類 MinClass
的后面后加上了 <T>
,這樣就聲明了泛型參數 T,作為一個變量可以是字符串類型,也可以是數字類型。
7. 泛型約束
語法:通過 extends
關鍵字來實現泛型約束。
如果我們很明確傳入的泛型參數是什么類型,或者明確想要操作的某類型的值具有什么屬性,那么就需要對泛型進行約束。通過兩個例子來說明:
interface User {
username: string
}
function info<T extends User>(user: T): string {
return 'imooc ' + user.username
}
代碼解釋: 示例中,第 5 行,我們約束了入參 user 必須包含 username 屬性,否則在編譯階段就會報錯。
下面再看另外一個例子:
type Args = number | string
class MinClass<T extends Args> {}
const m = new MinClass<boolean>() // Error, 必須是 number | string 類型
代碼解釋:
第 3 行,約束了泛型參數 T 繼承自類型 Args,而類型 Args 是一個由 number 和 string 組成的聯合類型。
第 5 行,泛型參數只能是 number 和 string 中的一種,傳入 boolean 類型是錯誤的。
8. 多重類型泛型約束
通過 <T extends Interface1 & Interface2>
這種語法來實現多重類型的泛型約束:
interface Sentence {
title: string,
content: string
}
interface Music {
url: string
}
class Classic<T extends Sentence & Music> {
private prop: T
constructor(arg: T) {
this.prop = arg
}
info() {
return {
url: this.prop.url,
title: this.prop.title,
content: this.prop.content
}
}
}
代碼解釋:
第 10 行,約束了泛型參數 T
需繼承自交叉類型(后續有單節介紹) Sentence & Music
,這樣就能訪問兩個接口類型的參數。
9. 小結
泛型在 TypeScript 中用途廣泛,可以靈活的控制類型之間的約束,提高代碼復用性,增強代碼可讀性。