# 🏅 Day 13 - type 泛型 泛型也很常用在 type 型別別名上,如以下範例: ```tsx= // 代入 Passthrough<T> 裡面的 T 為什麼樣的型別 // 賦值後的 T 就會成為該型別 type Passthrough<T> = T; // 使用 Passthrough 定義不同型別的變數 let numberValue: Passthrough<number> = 42; // 引發錯誤,型別衝突 let numberValue2: Passthrough<number> = "42"; let stringValue: Passthrough<string> = "Hello, world!"; let booleanValue: Passthrough<boolean> = true; let arrayValue: Passthrough<number[]> = [1, 2, 3, 4, 5]; // 使用這些變數 console.log(numberValue); // 輸出: 42 console.log(stringValue); // 輸出: "Hello, world!" console.log(booleanValue); // 輸出: true console.log(arrayValue); // 輸出: [1, 2, 3, 4, 5] ``` 當使用型別別名泛型時,TypeScript 不會自動去做型別推斷。所以需要在使用時,明確指定型別參數,例如: **`<number>`** 或 **`<string>`**。 ## **情境一:身高範例** 通常情況下,我們可能會這樣定義一個表示人的型別,其中包含身高屬性,並設定為 **`number`** 型別: ```tsx= type Person = { height: number; }; const mike: Person = { height: 180, }; ``` 以這例子中,**`mike`** 的身高被設定為一個數字 180。 ### **使用泛型的時機** 有可能在專案中,會同時需要用數字,或中文字例如「一百八十公分」來呈現,此時就需要比較靈活的型別定義。這樣泛型就派上用場了。 我們可以這樣定義一個泛型型別名: ```tsx= type FlexiblePerson<T> = { height: T; }; const mike1: FlexiblePerson<number> = { height: 180, }; const mike2: FlexiblePerson<string> = { height: '一百八十公分', }; ``` 在 **`mike1`** 的例子中,身高是以數字形式表示的,而在 **`mike2`** 中,身高則是以字串來呈現。 ## 情境二:水果物件表 以下是設計一個儲存商品列表資訊的功能。想要用 **`GenericValObj`** 來建立一個物件,該物件將以商品名稱作為 `key`,並以商品價格作為值。 ```tsx= // 新增泛型物件型別 // 屬性 key 必須為字串形式,可以無限制的增加 type GenericValObj<T> = { [key: string]: T; } // 如果 key 值有限定的內容,也可以透過 type 來進行組合 type BodyKey = "height" | "weight" | "bloodPressure" type BodyValue<T> = { [K in BodyKey]: T } // 定義 productPrices 儲存商品價格,value 為 number 型別 const productPrices: GenericValObj<number> = { "apple": 1.99, "banana": 0.99, "cherry": 2.49, }; // 新增更多商品價格 productPrices["grape"] = 1.79; // productPrices["orange"] = "三美元"; // 會出錯,因為泛型設定的型別參數為 number // 定義一個 productDescriptions 物件來儲存商品描述,value 為 string 型別 const productDescriptions: GenericValObj<string> = { "apple": "一個美味的水果", "banana": "一個黃色的水果", "cherry": "一個比較小又紅的水果", }; // 新增更多商品描述 productDescriptions["grape"] = "A small purple fruit"; ``` ## **情境三:API response 包裝** 如果你正在開發一個應用程式,需要與多個不同的 API 進行交互。這些 API 回傳不同型別的數據 例如: 1. 一些回傳數字類型的數據(如使用者的年齡) 2. 另一些則回傳字符串類型的數據(如使用者的姓名) 如果想要用統一方法來包裝這些 API response,可以這樣寫: ```tsx type ApiResponse<T> = { data: T; status: string; } // 使用範例 const ageResponse: ApiResponse<number> = { data: 30, status: "success" }; const nameResponse: ApiResponse<string> = { data: "Alice", status: "success" }; ``` 如果資料會需要管理 response 回傳成功或失敗的話,可以這樣寫: ```tsx= // 定義 User 介面 interface User { name: string; age: number; } // 定義 ApiResponse 泛型型別 type ApiResponse<T> = { success: boolean; message: string | null; data: T | null; } // 使用 User 介面的 ApiResponse const userResponse: ApiResponse<User> = { success: true, message: null, data: { name: "Alice", age: 30 } }; // 用於失敗的 ApiResponse,沒資料 const errorResponse: ApiResponse<null> = { success: false, message: "Error retrieving data", data: null }; ``` ## **情境四:網頁版本的 MP3 播放器** 假設你正在開發一個網頁版的 MP3 播放器。這個播放器需要許多不同的設定選項,例如音量控制、播放模式(例如隨機播放)、使用者設定等。 來試著用泛型來進行規劃: ```tsx= // 定義設定選項的 type 泛型 type ConfigOption<T> = { key: string; value: T; description: string; category: string; } // 使用範例 const volumeConfig: ConfigOption<number> = { key: "volume", value: 75, description: "控制播放器的音量大小", category: "音效設定" }; const shuffleModeConfig: ConfigOption<boolean> = { key: "shuffleMode", value: true, description: "啟用/停用隨機播放模式", category: "播放控制" }; const usernameConfig: ConfigOption<string> = { key: "username", value: "user123", description: "用戶的顯示名稱", category: "用戶設定" }; ``` ## 開發題 ### 1. 資料庫重構優化 請嘗試將 `UserEntry` 和 `ProductEntry` 重構,合併成一個自訂的 `type`,並使用泛型來進行優化 ```tsx= // 定義 UserEntry 和 ProductEntry 型別 type UserEntry = { id: string; data: { name: string; age: number; }; } type ProductEntry = { id: string; data: { title: string; price: number; }; } // 建立 UserEntry 和 ProductEntry 的函式 function createUserEntry(id: string, data: { name: string; age: number; }): UserEntry { return { id, data }; } function createProductEntry(id: string, data: { title: string; price: number; }): ProductEntry { return { id, data }; } // 使用函式來建立 UserEntry 和 ProductEntry const userEntry = createUserEntry("user1", { name: "John Doe", age: 30 }); const productEntry = createProductEntry("product1", { title: "Apple iPhone 13", price: 799 }); ``` ### 2. 外送訂單管理系統 你正在開發一個能夠管理外送訂單和餐點的系統,參考下面的部分程式碼,盡可能使用泛型和前面所學的其它技巧來優化程式碼內容 ```tsx= // 定義食物和飲料型別 type Food = { id: string; name: string; price: number; } type Drink = { id: string; name: string; sugarLevel: string; // 只包含 "standard", "half", "no" price: number; } // 定義訂單型別 type Order = { id: string; customerName: string; item: Food | Drink; } // 定義一個 OperationResult 代表完成一次操作後的回傳格式 type OperationResult<T> = { success: boolean; data: T; message: string; } const foods: Food[] = [] const drinks: Drink[] = [] const orders: Order[] = [] function addFood(food: Food): OperationResult<Food> { foods.push(food); return { success: true, data: food, message: "成功加入食物", }; } function addDrink(drink: Drink): OperationResult<Drink> { drinks.push(drink); return { success: true, data: drink, message: "成功加入飲料", }; } function addOrder(order: Order): OperationResult<Order> { orders.push(order); return { success: true, data: order, message: "成功加入訂單", }; } // 隨機生成 ID function genId(prefix: string): string { return `${prefix}-${Math.random().toString(36).slice(2, 8)}`; } function createFood(name: string, price: number): Food { return { id: genId("food"), name, price } } function createDrink(name: string, price: number, sugarLevel: string): Drink { return { id: genId("drink"), name, sugarLevel, price } } function createOrder(customerName: string, item: Food | Drink): Order { return { id: genId("order"), customerName, item, } } // 測試函式 const friedChicken = createFood("friedChicken", 100) const cola = createDrink("cola", 50, "standard") console.log(addFood(friedChicken)) console.log(addDrink(cola)) console.log(addOrder(createOrder("Jack", friedChicken))) ``` <!-- 解答: 1. type DataEntry<T> = { id: string; data: T } type User = { name: string; age: number; } type Product = { title: string; price: number; } // 建立 UserEntry 和 ProductEntry 的函式 function createUserEntry(id: string, data: { name: string; age: number; }): DataEntry<User> { return { id, data }; } function createProductEntry(id: string, data: { title: string; price: number; }): DataEntry<Product> { return { id, data }; } 2. // 定義食物和飲料型別 type Product = { id: string; name: string; price: number; } type SugarLevel = "standard" | "half" | "no" interface Food extends Product {} interface Drink extends Product { sugarLevel: SugarLevel } // 定義訂單型別 type Order = { id: string; customerName: string; item: Food | Drink; } // 定義一個 OperationResult 代表完成一次操作後的回傳格式 type OperationResult<T> = { success: boolean; data: T; message: string; } const foods: Food[] = [] const drinks: Drink[] = [] const orders: Order[] = [] type addObject<T> = (obj: T) => OperationResult<T> function getAddObjectFunc<T> (objList: T[], message: string): addObject<T> { return function (obj: T) { objList.push(obj) return { success: true, data: obj, message } } } const addFood = getAddObjectFunc<Food>(foods, "成功加入食物") const addDrink = getAddObjectFunc<Drink>(drinks, "成功加入飲料") const addOrder = getAddObjectFunc<Order>(orders, "成功加入訂單") // 隨機生成 ID function genId(prefix: string): string { return `${prefix}-${Math.random().toString(36).slice(2, 8)}`; } function createFood(name: string, price: number): Food { return { id: genId("food"), name, price } } function createDrink(name: string, price: number, sugarLevel: SugarLevel): Drink { return { id: genId("drink"), name, sugarLevel, price } } function createOrder(customerName: string, item: Food | Drink): Order { return { id: genId("order"), customerName, item, } } // 測試函式 const friedChicken = createFood("friedChicken", 100) const cola = createDrink("cola", 50, "standard") console.log(addFood(friedChicken)) console.log(addDrink(cola)) console.log(addOrder(createOrder("Jack", friedChicken))) -->