昨天我們提到了泛型(generics)的使用,但泛型就像一個型別為 any
的變數一樣,使用者愛帶什麼型別都可以,基本上是沒有型別上的限制,但有些時候我們想要使用泛型,讓函式或 type alias 可以不只適用於一種型別,但有希望能對使用者帶入的型別有一點限制的話,可以怎麼做呢?
在 TypeScript 中提供了「泛型限制(Generic Constraints)」的用法,語法上只需要使用 extends
就可以了!
一般 TypeScript 的初學者看到 extends
時,直覺上會想到的是可以拿來擴展某一個介面(interfaces)使用,像是這樣:
interface Person {
age: number;
occupation: string;
}
// 使用 extends 來擴展另一個 interface
interface Author extends Person {
firstName: string;
lastName: string;
}
就可以建立一個新的 Author
interface,且讓它帶有 Person
中所定義的屬性:
const aaron: Author = {
age: 33,
occupation: 'developer',
firstName: 'PJ',
lastName: 'Chen',
};
或者另一個很多人會想到的是 JavaScript 中「類別繼承(class extends)」的使用,例如:
class Square {
constructor(public width: number) {}
}
// 使用 extends 來繼承另一個 class 的屬性
class Rectangle extends Square {
constructor(width: number, public height: number) {
super(width);
}
}
const square = new Square(10);
const rectangle = new Rectangle(10, 20);
然而,在 TypeScript 中的 extends
除了上述用法外,還被賦予了更多的功能,像是可以用來限制泛型可被帶入的型別(generic constraints)或是作為型別的條件判斷(conditional types)。在這種情況下,extends
比較好理解的中文應該是「需要滿足 ooo」,但更精確的是指「是 ooo 的子集合」。今天就先來看一下如何透過 extends 來限制泛型可被帶入的型別。
extends
在建立 Type Utility 是非常容易用到,因此我們在後面幾天也會一直看到它。
先來看一下昨天寫的函式:
function getFirstElement<T>(arr: T[]): T {
const [firstElement] = arr;
return firstElement;
}
假設現在我們希望限制這個 T 只能是數值(number)的話,可以搭配 extends 寫成 <T extends number>
,意思就是限制使用者帶入的泛型 「T 需要時 number 的子集合」:
更精確的來說,應該是指「T
要是 number
的子集合(subset)」,如果用集合的圖示來表達的話,會像這樣:
這時候如果我們在呼叫 getFirstElement
時,帶入的卻是 string[]
的話,TS 就會報錯,因為 T 現在是 string,但 T 並是 number 的子集合:
畫成圖的概念會像這樣:
同樣的,如果是希望泛型 T 只能帶入 string 或 number 的話,則可以寫成 <T extends number | string>
,意思就是 T 這個泛型不能什麼都接受,它需要時 string 或 number 的子集合才行,像是這樣:
這時候如果使用者帶入的泛型不是 number 或 string 的話 TS 就會報錯。例如,下圖帶的是 boolean:
到這裡你可能雖然知道了「喔~原來 extends
還能當成『需要滿足 ooo』」的意思,但卻還不知道實際的使用時機。
關於這點我們會在後面幾天看到很多實際的例子,這裡先提供一個簡單的範例,假設有一個函式可以輸出姓名,它可以:
firstName
和 lastName
這兩個屬性一開始可能會這樣寫這個 function:
function logPersonName<T>(person: T) {
return `${person.firstName} ${person.lastName}`;
}
但這時候因為 TypeScript 沒辦法確保泛型 T
中一定有 firstName
和 lastName
這兩個屬性,因此會報錯:
這時候就可以透過 generic constraints 的方式,限制使用者帶入的泛型的型別至少要包含 firstName
和 lastName
這兩個屬性,其他的屬性 TypeScript 則不管。
可以寫成這樣:
interface PersonName {
firstName: string;
lastName: string;
}
// 使用 T extends PersonName,限制 T 一定要是 PersonName 型別的子集合
function logPersonName<T extends PersonName>(person: T) {
return `${person.firstName} ${person.lastName}`;
}
這時候因為能夠確保帶入 function 參數的泛型 T 一定有 firstName
和 lastName
這兩個屬性,所以 TypeScript 就不會再報錯,使用者也可以帶入任何物件,只要這個物件中包含這兩個必要的屬性:
// 只要使用者帶入的物件包含 firstName 和 lastName 就好(符合對泛型的限制)
// 其他多餘的物件屬性 TypeScript 不會管
logPersonName({
firstName: 'Aaron',
lastName: 'Chen',
occupation: 'developer',
});
logPersonName({
firstName: 'PJ',
lastName: 'Chen',
favorite: 'smart doctor',
});
但如果帶入的物件少了 firstName
或 lastName
,則 TS 就會直接報錯:
如果有仔細閱讀的讀者,應該會發現讀到這裡好像哪裡怪怪的,最一開始的例子是 Primitive Type,如果用 T extends number
的話,這個 T 就只能是 number
,不能是 string | number
;但如果是 Object 的話,當使用 T extends {firstName: string}
,這時候即使 T 是 {firstName: string, lastName: string}
也是可以的。這樣的情況在 TypeScript 中的 Unions and Intersection Types 也可以看到類似的現象。
關於使用 extends
來限制泛型可被接受型別的用法同樣適用在 type alias 上,例如:
type PersonNameType {
firstName: string;
lastName: string
}
type Person<T extends PersonNameType> = T;
意思一樣是泛型 T
可以是任何型別,但它至少要是 PersonName
這個型別的子集合,也就是要有 firstName
和 lastName
這兩個屬性。使用時會像這樣:
/**
* T 等於
* {
* firstName: string;
* lastName: string;
* occupation: string;
* }
* */
const pjchender: Person<{
firstName: string;
lastName: string;
occupation: string;
}> = {
firstName: 'PJ',
lastName: 'Chen',
occupation: 'developer',
};
後面我們會再看到更多例子,到時候會更清楚 extends
在泛型中的使用。
https://tsplay.dev/Wv89rN @ TypeScript Playground
這最好是日記...有備而來吧...
真的是剛剛寫的啦...
之前 iphone 就還沒發表吼
我還跟隊友說我這樣不行,明明講一句話不用十分鐘就解釋完的東西,怎麼寫了快 2hr XD
用講得比較快XD
意思一樣是泛型 T 可以是任何型別,但它至少要滿足
PersonName
這個屬性,也就是要有firstName
和lastName
這兩個屬性。
這裡的 PersonName
正確來說是型別對吧?
另外在上面的例子 PersonName
是 interface,後來變成 type。雖然知道兩者常互換使用,不過有沒有什麼使用上的原則或限制呢?
PersonName
是型別沒錯喔,已修正,謝謝 td!
根據 TypeScript 官方網站的描述:
For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs something to be the other kind of declaration. If you would like a heuristic, use interface until you need to use features from type. (Everydate Types)
以我個人的經驗來說,多數時候會用 interface,但如果是要用一個 Type 來建立另一個 Type 的時候(Type Utility),則會用 Type 來做定義。
原來如此!