跳至主要内容

[Day02] TS: 泛型(Generics)能幹嘛?

「泛型(Generics)」是 TypeScript 中很常會使用到的功能,泛型的概念簡單來說,就是讓「型別」也變成一個變數,可以根據不同的情況套用不同的型別。因為在強型別的語言中,不論變數或函式的回傳值的型別,都會需要被加以定義。

舉例來說,如果我們寫一個能夠回傳陣列第一個元素的函式,我們會需要這樣寫:

function getFirstElementOfNumberArray(arr: number[]): number {
const [firstElement] = arr;
return firstElement;
}

你會發現,我們會需要定義這個函式所接受的參數型別,例如這裡是 number[],且函式回傳值的型別因為是陣列中的第一個元素,所以也會是 number

這時候我們可以這樣用:

const firstElement = getFirstElementOfNumberArray([1, 9, 8, 8]); // 1

但你會發現因為型別定義的關係,這個函式只能接受 number[] 作為它的參數,如果你是想要取出 string[] 的話,因為型別合,TS 會報錯,像是這樣:

const firstElement = getFirstElementOfNumberArray([
'Oh.So.Pro.',
'就。很。Pro',
'非常。Pro',
]);

你原本可能會預期它應該要取出陣列中的第一個元素給你,也就是「Oh.So.Pro.」,結果 TS 在編譯時卻發生錯誤:

TypeScript Generics

這個錯誤並不是因為我們在程式中使用了中文字,而是因為一開始我們定義 getFirstElement 這個函式,只能接受型別為 number[] 的參數,但現在我們卻帶入了型別為 string[] 的參數。

如果沒有泛型的話,我們變成需要針對 string[] 的參數多寫一個函式,取名為 getFirstElementOfStringArray 像是這樣:

// 定義一個能夠接受 string[] 作為參數的函式
function getFirstElementOfStringArray(arr: string[]): string {
const [firstElement] = arr;
return firstElement;
}

如此就能正確呼叫這個函式:

const firstElement = getFirstElementOfStringArray([
'Oh. So. Pro.',
'就很 Pro',
'非常 Pro',
]);
console.log(firstElement); // "Oh. So. Pro."

這時候你會發現,在不管型別的情況下,getFirstElementOfNumberArraygetFirstElementOfNumberArray 這兩個函式明明就內容是完全相同的,但卻因為型別的限制,逼著我們要拆成兩個不同的函式。

好在,在 TypeScript 中有泛型可以使用,前面有提到,泛型的概念其實就像是把型別也變成一個變數,回到上面這兩個 function 來看,你會發現這兩個函式完全只差在「型別」是不同的(下圖粉色框框):

TypeScript Generics

因此這時候我們可以做一個抽象化的動作,把這個型別也變成一個變數,這裡取名叫 T,就可以把這兩個只有型別不同的函式整合成一個,變成這樣:

function getFirstElement<T>(arr: T[]): T {
const [firstElement] = arr;
return firstElement;
}

如此在使用 getFirstElement 時,你可以把型別當成變數一樣,放入 <T> 的 T 中,明確告訴 TypeScript 這時候的函式參數型別是什麼,像是這樣:

TypeScript Generics

有了泛型的好處是,你將不在需要只因為型別的不同而要才成多個不同的函式。

更重要的是,一般來說,我們不需要明確在 <> 中告訴 TS 使用的參數型別是什麼,而是可以讓 TS 自己推導(稱作:type argument inference),因此,我們更常直接這樣寫:

// TS 會自動根據帶入的參數推導這裡的 T 是 number
const firstElement1 = getFirstElement([1, 9, 8, 8]);

// TS 會自動根據帶入的參數推導這裡的 T 是 string
const firstElement2 = getFirstElement(['Oh.So.Pro.', '就。很。Pro', '非常。Pro']);

當我們把滑鼠移到函式上方時,可以看到 TS 自動推導的結果:

TypeScript Generics

Type Alias 也能使用泛型

除了在函式中可以使用泛型外,在 Type Alias 中也可以使用泛型的概念。

假設一般來說 age 的型別是 number

type Person = {
age: number;
};

const john: Person = {
age: 20,
};

但某種形況下,age 會用中文字來表示,例如「二十」,這時候你會發現再次發生型別的錯誤,因為我們試圖把 string 帶入型別為 number 的欄位中:

TypeScript Generics

這時候,如果沒有泛型的話,將只能像這樣建立兩個不同的型別:

type PersonWithNumberAge = {
age: number;
};

type PersonWithStringAge = {
age: string;
};

但有了泛型的話,更好的方式就是使用泛型,像這樣:

type Person<T> = {
age: T;
};

使用時就可以這樣:

const john1: Person<number> = {
age: 30,
};

const john2: Person<string> = {
age: '二十',
};

由於 type alias 並不像函式一樣有 type argument inference,因此需要明確把型別定義在 <> 中,如上面的 <number><string>

透過泛型,能夠把型別當成變數使用,是不是非常方便啊!

範例程式碼

https://tsplay.dev/m3z43N @ TypeScript Playground

參考資料