[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 在編譯時卻發生錯誤:
這個錯誤並不是因為我們在程式中使用了中文字,而是因為一開始我們定義 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."
這時候你會發現,在不管型別的情況下,getFirstElementOfNumberArray
和 getFirstElementOfNumberArray
這兩個函式明明就內容是完全相同的,但卻因為型別的限制,逼著我們要拆成兩個不同的函式。
好在,在 TypeScript 中有泛型可以使用,前面有提到,泛型的概念其實就像是把型別也變成一個變數,回到上面這兩個 function 來看,你會發現這兩個函式完全只差在「型別」是不同的(下圖粉色框框):
因此這時候我們可以做一個抽象化的動作,把這個型別也變成一個變數,這裡取名叫 T
,就可以把這兩個只有型別不同的函式整合成一個,變成這樣:
function getFirstElement<T>(arr: T[]): T {
const [firstElement] = arr;
return firstElement;
}
如此在使用 getFirstElement
時,你可以把型別當成變數一樣,放入 <T>
的 T 中,明確告訴 TypeScript 這時候的函式參數型別是什麼,像是這樣:
有了泛型的好處是,你將不在需要只因為型別的不同而要才成多個不同的函式。
更重要的是,一般來說,我們不需要明確在 <>
中告訴 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 自動推導的結果:
Type Alias 也能使用泛型
除了在函式中可以使用泛型外,在 Type Alias 中也可以使用泛型的概念。
假設一般來說 age
的型別是 number
:
type Person = {
age: number;
};
const john: Person = {
age: 20,
};
但某種形況下,age 會用中文字來表示,例如「二十
」,這時候你會發現再次發生型別的錯誤,因為我們試圖把 string 帶入型別為 number 的欄位中:
這時候,如果沒有泛型的話,將只能像這樣建立兩個不同的型別:
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
參考資料
- Generics @ TypeScript > Type Manipulations
- Type Alias @ TypeScript > Everyday Types