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