# 🏅 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} } -->