跳到主要内容

用 extends 做為泛型限制(Generic Constraints)

// T extends number,表示 T 需要滿足 number
function getFirstElement<T extends number>(arr: T[]): T {
const [firstElement] = arr;
return firstElement;
}

假設現在我們希望限制這個 T 只能是數值(number)的話,可以搭配 extends 寫成 <T extends number> ,意思就是限制使用者帶入的泛型 「T 需要時 number 的子集合」:

更精確的來說,應該是指「T 要是 number 的子集合(subset)」,如果用集合的圖示來表達的話,會像這樣:

generics_extends

這時候如果我們在呼叫 getFirstElement 時,帶入的卻是 string[] 的話,TS 就會報錯,因為 T 現在是 string,但 T 並是 number 的子集合:

generics_extends_err1

畫成圖的概念會像這樣:

generics_extends2

同樣的,如果是希望泛型 T 只能帶入 string 或 number 的話,則可以寫成 <T extends number | string>,意思就是 T 這個泛型不能什麼都接受,它需要時 string 或 number 的子集合才行,像是這樣:

// T extends number,表示 T 需要滿足 number | string
function getFirstElement<T extends number | string>(arr: T[]): T {
const [firstElement] = arr;
return firstElement;
}

這時候如果使用者帶入的泛型不是 number 或 string 的話 TS 就會報錯。例如,下圖帶的是 boolean:

generics_extends_err2

Interface 範例

假設有一個函式可以輸出姓名,它可以:

  1. 接受「任何型別的物件」當作參數
  2. 但因為它要輸出姓名,所以參數本身有一個限制,就是物件中至少要有 firstNamelastName 這兩個屬性

這時候就可以透過 generic constraints 的方式,限制使用者帶入的泛型的型別至少要包含 firstNamelastName 這兩個屬性,其他的屬性 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 一定有 firstNamelastName 這兩個屬性,所以 TypeScript 就不會再報錯,使用者也可以帶入任何物件,只要這個物件中包含這兩個必要的屬性:

// 只要使用者帶入的物件包含 firstName 和 lastName 就好(符合對泛型的限制)
// 其他多餘的物件屬性 TypeScript 不會管

logPersonName({
firstName: 'Aaron',
lastName: 'Chen',
occupation: 'developer',
});

logPersonName({
firstName: 'PJ',
lastName: 'Chen',
favorite: 'smart doctor',
});

type alias(型別別名)範例

關於使用 extends 來限制泛型可被接受型別的用法同樣適用在 type alias 上,例如:

type PersonNameType {
firstName: string;
lastName: string
}

type Person<T extends PersonNameType> = T;

意思一樣是泛型 T 可以是任何型別,但它至少要是 PersonName 這個型別的子集合,也就是要有 firstNamelastName 這兩個屬性。使用時會像這樣:

/**
* T 等於
* {
* firstName: string;
* lastName: string;
* occupation: string;
* }
* */
const pjchender: Person<{
firstName: string;
lastName: string;
occupation: string;
}> = {
firstName: 'PJ',
lastName: 'Chen',
occupation: 'developer',
};