extends 在 TS 裡是一個非常重要的關鍵字,並且分別用在兩種完全不同的地方 (就像你很辣/這道食物好辣,都是辣卻完全不同),所以想特別寫一篇筆記他們的不同

  • Generic Constraints: 用在 generic 中,把 Type parameters 約束在特定一個範圍內
  • Extend Interfaces: 允許你的 interface 繼承另一個 interface 的所有屬性型別
🔖 文章索引

1. extends as constrain
2. Inheritance with extends

extends as constrain

narrow the scope of a generic (Type parameters) to make it more useful. 把 Type parameters 約束在特定一個範圍內

T 的 type 必須為 B / 確保 T 有 {name: string} 這個型別屬性 / T 的 type 必須為 number

generics 幫助我們寫出 reusable 的程式碼,雖然上一章提到當你 “不確定傳進來的是什麼型別” 就很適合用 generics,但我們還是不希望每次 Type parameters 都為 any ,很多時候應該被限制在一個特定範圍內,這時就是 extends 的出場時機啦

首先讓我們看一個未使用 extends 前的例子

// T 可以是任何型別
function printName<T>(someone: T): T {
return someone
}

printName("hello world")
// "hello world"

printName(123)
// 123

printName({name: "peter"})
// {name: "peter"}

T (Type parameters) 為 any,傳進來的值不管什麼型別都不會報錯。但若把函式 printName 改一下就會出現錯誤了,因為 T 沒有 name 這個屬性。

function printName<T>(someone:T){
return someone.name
}

// ^? Property 'name' does not exist on type 'T'

要解決這個錯誤,必須使用 extends 把 T 限制在特定 type 裡。用生活例子解釋就是 「將 T 型別參數設了一個門禁系統(extends),只有擁有/符合 {name: string}的人才能進去。」

function printName<T extends {name: string}>(someone:T){
return someone.name
}

// ^? prontName(someone: { name: string; }): string
printName({name:'hannah'}) // ok
printName({name:'hannah', age: 12}) // ok
printName({age: 12}) // error

實務上再處理物件資料時也常使用 extends 去規範函式中參數的 type

/**
* 範例:
* const array = [{ name: 'Alice', age: 20 }, { name: 'Bob', age: 20 }, { name: 'Charlie', age: 30 }];
* filterByProperty(array, 'age', 20) 應該回傳 [{ name: 'Alice', age: 20 }, { name: 'Bob', age: 20 }]
*
* @param {array} lists - 一個物件的陣列
* @param {string} prop - 要過濾的屬性名稱
* @param val - 要過濾的屬性值
* @returns {array} - 回傳過濾後的陣列
*/

export function filterByProperty<T, K extends keyof T>(
lists: T[],
prop: K,
val: T[K]
) {
return lists.filter((list) => list[prop] === val);
}
  • T: 函式的第一個參數,表示任何一個 T 型別的物件。
  • key: K:函式的第二個參數,代表 obj 物件中的一個key(屬性名)。確保了傳入的 key 必須是物件 obj 中存在的屬性。
  • T[K]:表示函式的回傳值將與物件 objkey 的值的型別相同。也就是利用到 Indexed Access Types 的技巧,允許根據 key 來獲得對應的值型別。

運用這些甚至可以更進階去規範 object 不同層的參數

const obj = {
foo: {
a: true,
b:2
},
bar: {
c: "cool",
d: 2
}
}

const getDeepValue = <
Obj,
FirstKey extends keyof Obj,
SecondKey extends keyof Obj[FirstKey]
>(
obj: Obj,
firstKey: FirstKey,
secondKey: SecondKey
) => {};

getDeepValue(obj, "foo", "a");

小結論

善用 extends 限制 key 必須在特定範圍內可以讓程式碼錯誤降到最低,例如嘗試訪問不存在的屬性。並且因為 generics 可以用於任何型別所以 generic function/ generic type 也可以一直被 reuse 讓程式碼保持安全又簡潔。

Conditional Types

三元運算式的 Conditionals Types 也是使用 extends

A 的 type 為 B / 鳳梨的 type 為水果 / 3 的 type 為 number
type A1 = 3 extends number ? number : string; // number
type A2 = 's' extends number ? number : string; // string
type A3 = 'x' | 'y' extends 'x' ? string : number; // number

type P<A, B> = A extends B ? true : false;
type Result1 = P<3, number> // true
type Result2 = P<number, 3> // false
type Result3 = P<'s', 123> // false

A extends B 若 A 的 type 為 B, IsAssignableTo就會回傳 true,反之 false

鳳梨是水果一種 true / 水果是鳳梨一種 false

運用 Conditionals Types 就可以衍伸許多好用的 Utility Type,下一篇也會介紹 TS 內建的 Extract 跟 Exclude

Inheritance with extends

copy the properties and methods of one interface to another. 直接繼承另一個 interface 的所有屬性型別

在 TS 系列第一篇 [TypeScript] The Very Basics for TS 的 Union Type 其實就舉過不少 Inheritance with extends 的例子,他的用法也非常簡單只是要注意的是

`extends` Interfaces 顧名思義只能用在 interface ; type 不行

interface User {
name: string;
email: string;
}

interface Permission {
read: boolean;
write: boolean;
}

// 繼承 User 以及 Permission 的 interface
interface Admin extends User, Permission {
delete: boolean
}

// 繼承 User 的 interface
interface Guest extends User {
isGuest: boolean;
}


const admin: Admin = {
name: 'Hannah',
email: 'xxx',
read: true,
write: true,
delete: true
}

const guest: Guest = {
name: 'Hannah',
email: 'xxx',
isGuest: true
}

--

--