---
# System prepended metadata

title: "\U0001F3C5 Day 12- 泛型約束 extends"
tags: [TypeScript 直播班-2025]

---

# 🏅 Day 12- 泛型約束 extends

泛型約束允許我們為型別參數 `T` 定義一個約束條件。這樣做的目的是為了確保泛型不僅能代表任何型別，還能滿足特定的接口或擁有某些屬性，在實際使用上會用 `extends` 關鍵字來實做出這種約束條件，下方是一些範例程式碼的分享：

## 範例一：確保泛型是數字

在這個範例中，我們創建一個函式來翻倍一個數字。我們將使用泛型約束來確保傳入的參數是一個數字。

```tsx=
function doubleValue<T extends number>(value: T){
    return value * 2;
}

// 正常使用
const doubleNum = doubleValue(10); // 回傳 20

// 錯誤使用（將導致編譯錯誤，因為 'Hello' 不是數字）
// const notANumber = doubleValue("Hello");

```

原本可以放任意型別的`T`，多加上了`extends` 為 `<T extends number>`

這句話用白話文來講就像是：「**將 `T` 型別參數設了一個門禁系統(extends)，只有數字才能進去。**」

以致於後面函式代入 `srting` 字串時會出錯。


## 範例二： 確保代入型別擁有對應屬性

在前一個章節，你會發現到型別參數 `<T>` 可以代入各種型別，看似好用，但會有個潛在問題如下：


```tsx=
function returnWithLength<T>(arg: T): T {
    console.log(arg.length);  // 錯誤：型別 'T' 沒有 length 屬性
    return arg;
}

// 執行函式
returnWithLength([1, 2, 3]); 
// 這裡會產生錯誤
// 因為 TypeScript 不知道 'T' 是否含有 'length' 屬性
```

你可以將上面程式碼張貼到自己的編輯器，在這個範例中，函式 **`returnWithLength`** 嘗試讀取泛型 **`T`** 的 **`length`** 屬性。

這是因為 TypeScript 編譯器無法確定傳入的參數是否包含 **`length`** 屬性，以致於導致編譯錯誤。

這時候泛型约束（Generic Constraints）就派上用場了：


```tsx=
function returnWithLength<T extends { length: number }>(arg: T): T {
    console.log(arg.length);  // 正確：現在 TypeScript 知道 'T' 包含 'length' 屬性
    return arg;
}


returnWithLength([1, 2, 3]); // 正確：陣列有 'length' 屬性
returnWithLength("Hello World"); // 正確：字串也有 'length' 屬性

// 以下將導致編譯錯誤，因為數字不包含 'length' 屬性
// returnWithLength(123);
```


**`{ length: number }`** 就是確保傳入的值，能如何該屬性與對應的型別

像是陣列有 `length` 屬性，型別是 `number`
```javascript=
console.log([1,2,3].length)
// 陣列有 length 屬性，所以會印出 3，代表陣列長度為 3
```
字串也有 `length` 屬性，型別也是 `number`
```javascript=
console.log("hello".length);
// 字串也有 length 屬性，印出 5，表示字串長度為 5
```

但數字就會出錯了，因為數字型別裡沒有屬性 `length`;

```javascript=
console.log(12345.length)
// 印出 Uncaught SyntaxError: Invalid or unexpected token
```


這種約束在處理陣列、字串或任何帶有 **`length`** 屬性的物件時特別有用，因為它使函式能夠安全地存取 **`length`** 屬性，同時又保持泛型的靈活性。


## 範例三：確保物件擁有特定屬性或方法

當希望傳入的物件不僅僅是某個型別，而且還必須擁有特定的屬性或方法時，泛型約束非常有用。這在建立通用函式庫蠻實用的。

例如你正在建立一個函式，該函式需要處理各種不同的資料物件，但**每個物件都必須有一個共同的 **`id`** 屬性時**。泛型約束就能派上用場。

```tsx
interface Identifiable {
  id: string;
}

function processItem<T extends Identifiable>(item: T) {
  console.log(`處理物件，ID 為: ${item.id}`);
  // ... 其他邏輯
}

// 會正常運作，因為物件符合 Identifiable 介面
processItem({ id: '123', name: '項目1' });

// 會編譯錯誤，因為缺少 id 屬性
processItem({ name: '項目2' });

```

在這個例子中，任何擁有 **`id`** 屬性的物件都可以被 **`processItem`** 函式接受，在 TypeScript 中，**只要物件滿足了介面的最小要求，它就被認為是符合該介面的**，即使它有額外的屬性也沒問題。


這樣設計方式，就能開發一個既靈活又嚴謹的方法，藉此處理各種不同的資料結構。


## 實作題

### **1. 計算陣列總和**

開發一個函式 **`calculateSum`**，並接受一個數字陣列作為參數，來計算出其總和。請使用泛型約束來確保傳入的參數必須是數字陣列。

```tsx=
// 請完成並改寫為泛型函式，並使用泛型約束
function calculateSum{
    return ???
}

// 正常使用
const total = calculateSum([1, 2, 3, 4]); // 回傳 10

// 錯誤使用（會編譯錯誤，因為參數不是數字陣列）
// const errorTotal = calculateSum(["a", "b", "c"]);
```

### **2. 篩選擁有特定屬性的物件陣列**

開發一個函式 **`filterItems`**，該函式接受一個物件陣列，並回傳所有具有特定屬性的物件。使用泛型約束來確保每個物件至少包含該屬性。

```tsx=
interface WithName {
    name: string;
}
// 請調整為泛型函式，並使用 WithName 做為泛型約束條件，確保每個物件至少包含該屬性。
function filterItems(){
    return items.filter(item => item.name.startsWith("特定條件"));
}

// 正常使用
const filteredItems = filterItems([{ name: '特定條件物件1' }, { name: '其他物件' }]);

// 錯誤使用（將導致編譯錯誤，因為陣列中的物件沒有 name 屬性）
// const errorFilteredItems = filterItems([{ id: 1 }, { id: 2 }]);
```

### **3. 使用泛型處理不同型別的資料**

在 Day 11 中有練習到使用泛型來處理不同型別的資料，而今天的練習則是進一步加入泛型約束來簡化程式碼的內容。開發一個函式 **`processArray`**，該函式接受一個泛型陣列 **`array: Array<T>`**，其中 **`T`** 可以是 **`number`**、**`string`** 或 **`boolean`**。針對不同型別的元素處理方式如下：
* **`number`**：將數字乘以 2 後回傳
* **`string`**：將字串轉為全大寫
* **`boolean`**：將布林值進行反轉後回傳

使用泛型約束來確保傳入的物件符合此結構，另外也可以自行定義型別別名來精簡程式碼。

```tsx=
// 請完成並改寫為泛型函式，並使用泛型約束
function processArray(items){
  return ???
}

// 使用這個函式
const processedStrings = processArray<string>(["apple", "banana"]);
// 輸出: ["APPLE", "BANANA"]
const proceedingNums = processArray<number>([1, 2, 3])
// 輸出: [2, 4, 6]
const proceedingBooleans = processArray<boolean>([true, false, true])
// 輸出: [false, true, false]
```

### **4. 用於資料處理的函式**

開發一個函式 **`processData`**，該函式接受一個包含 **`data`** 和 **`timestamp`** 屬性的物件。使用泛型約束來確保物件包含這些屬性。

```tsx
interface DataWithTimestamp {
    data: any;
    timestamp: Date;
}

// 請改寫為泛型函式，並使用泛型約束
function processData(item) {
    console.log(`資料: ${item.data}, 時間戳: ${item.timestamp}`);
}

// 正常使用
processData({ data: "some data", timestamp: new Date() });

// 錯誤使用（將導致編譯錯誤，因為缺少 timestamp 屬性）
// processData({ data: "some data" });
```


### **5. 合併物件**

開發一個函式 **`mergeObject`**，該函式接受兩種不同的 **`object`** 作為參數，並將兩個物件進行合併後回傳。使用泛型約束來確保輸入的物件符合型別定義。

```tsx=
// 請完成並改寫為泛型函式，並使用泛型約束
function mergeObject(obj1, obj2){
  return ???
}

// 使用這個函式
type CSSFont = {
  fontSize: string;
  fontFamily: string;
}

type CSSBox = {
  width: string;
  height: string;
}

const font: CSSFont = {
  fontFamily: "Fira Code",
  fontSize: "20px"
}

const box: CSSBox = {
  width: "100px",
  height: "50px"
}

const cssSettings = mergeObject<CSSFont, CSSBox>(font, box)
// 輸出：
// {
//  "fontFamily": "Fira Code",
//  "fontSize": "20px",
//  "width": "100px",
//  "height": "50px"
// } 
```



<!-- 解答：  

1. 
function calculateSum<T extends number>(arr: T[]){
    return arr.reduce((a, b) => a + b, 0);
}

2. 
interface WithName {
    name: string;
}

function filterItems<T extends WithName>(items: T[]){
    return items.filter(item => item.name.startsWith("特定條件"));
}

3. 
type processType = number | string | boolean;

function processArray<T extends processType>(items: T[]): processType[] {
  const typeOfItem = typeof items[0];

  if (typeOfItem === "number"){
    return (items as number[]).map(item => item * 2);
  }

  if (typeOfItem === "string"){
    return (items as string[]).map(item => item.toUpperCase());
  }

  return (items as boolean[]).map(item => !item);
}

4.
function processData<T extends DataWithTimestamp>(item: T) {
    console.log(`資料: ${item.data}, 時間戳: ${item.timestamp}`);
}

5. 
function mergeObject<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return {...obj1, ...obj2}
}

-->
