# 🏅 Day 8 - 型別別名(type) ## 型別別名(Type Aliases) `型別別名(Type Aliases)` 允許為任何型別定義一個新名稱。不僅僅是簡單的重新命名,它還允許你建立更複雜、更具表達力的型別結構,尤其是複雜的物件或聯合型別。 ### 範例 1: 基本型別別名 ```tsx type StringOrNumber = string | number; function logMessage(message: StringOrNumber): void { console.log(message); } logMessage("Hello, TypeScript!"); logMessage(42); ``` 在這個範例中,**`StringOrNumber`** 是一個型別別名,代表 **`string | number`** 的聯合型別。 ### 範例 2: 物件型別別名 ```tsx type User = { name: string; age: number; isActive: boolean; }; function printUser(user: User): void { console.log(`Name: ${user.name}, Age: ${user.age}, Active: ${user.isActive}`); } printUser({ name: "洧杰", age: 18, isActive: true }); ``` 在這個範例中,**`User`** 是一個型別別名,用於描述一個具有 **`name`**、**`age`** 和 **`isActive`** 屬性的物件。 ## 常用情境一:重複使用的複雜型別 ### 案例:蝦皮產品 假設你正在開發一個線上商店的應用程式,其中需要處理多種產品資訊。每個產品都有一組共同的屬性,例如名稱、價格和庫存數量。在不同的功能中,你可能需要重複使用這些產品的屬性結構。 ### 定義產品型別別名 ```tsx type Product = { id: number; name: string; price: number; stock: number; }; ``` **`Product`** 型別別名被用來表示商店中的一個產品。這個型別別名包含了所有產品共用的屬性:**`id`**、**`name`**、**`price`** 和 **`stock`**。 ### 範例程式碼 ```tsx type Product = { id: number; name: string; price: number; stock: number; }; // 重要:建立空的產品陣列,有發現型別註釋套用在陣列上,改為 Product 型別別名了嗎? let products: Product[] = []; // 定義一個函式來新增產品到陣列中 // 需符合 Product type 的型別別名 function addProduct(product: Product) { products.push(product); } // 使用 addProduct 函式來新增產品 addProduct({ id: 101, name: '手機', price: 29999, stock: 50 }); addProduct({ id: 102, name: '耳機', price: 999, stock: 100 }); // 顯示產品陣列 console.log(products); ``` 在這個情境中,`Product` 型別別名被用於多個函式中來代表產品資訊。 而且在需要修改產品屬性結構時,只需要更新 `Product` 型別別名就好,就不用去修改每一處使用到這些屬性的代碼。 這就是使用 `type` 來定義重複使用的複雜型別的優勢所在 :D ## 常用情境二:增加程式碼可讀性 ### 案例:UI 元件狀態管理 假如你在開發一個具有多種狀態的 UI 元件,例如一個按鈕,它可以處於不同的狀態,如「正常」、「禁用」、「載入中」等。除了前面有提到的 `enum` 之外,我們也可以使用型別別名來清晰定義這些狀態。 ### 定義按鈕狀態的型別別名 首先定義一個型別別名,來表示按鈕的不同狀態: ```tsx= type ButtonState = 'normal' | 'disabled' | 'loading'; ``` 這裡 **`ButtonState`** 型別別名包含了按鈕可能的所有狀態。 ### 使用按鈕狀態型別別名 接著,我們可以使用這個型別別名來管理 UI 元件的狀態: ```tsx= function renderButton(state: ButtonState) { switch (state) { case 'normal': console.log('渲染正常狀態的按鈕'); break; case 'disabled': console.log('渲染禁用狀態的按鈕'); break; case 'loading': console.log('渲染載入狀態的按鈕'); break; default: console.log('未知的按鈕狀態'); } } // 測試不同的按鈕狀態 renderButton('normal'); renderButton('disabled'); renderButton('loading'); ``` **`renderButton`** 函式根據傳入的 **`ButtonState`** 狀態來決定如何渲染按鈕。這種方式使得狀態管理變得更加清晰和簡單 如果需要增加新的按鈕狀態,只需在 **`ButtonState`** 型別別名中增加新的狀態,並在 **`renderButton`** 函式中加入對應的處理邏輯即可 這樣的型別別名定義還有助於避免錯誤,例如不小心將錯誤的狀態傳遞給按鈕,因為 TypeScript 的型別檢查會在開發、編譯時補捉到這些錯誤。 ## 常用情境三:條件型別(Conditional Types) 在 TypeScript 中,**`extends`** 關鍵字用於條件型別的定義中,這是一種特殊的語法,它允許我們根據某個條件來選擇兩種型別之一。 下面來提供範例,條件型別的一般形式是: ```tsx A extends B ? C : D ``` 這裡的意思是,**`A`** 和 **`B`** 是兩種型別,**`C`** 和 **`D`** 也是型別。 如果 **`A`** 可以賦值給 **`B`**,那麼此條件型別的結果是 **`C`** 型別,否則是 **`D`** 型別。 在建立基於條件的型別時(例如,根據某些條件選擇不同的型別),型別別名可以用來定義這類型複雜邏輯。 直接來上案例~ ### 案例:基於使用者角色選擇不同的權限型別 假使你正在開發一個系統,其中使用者根據他們的角色(例如說管理員或一般使用者)都擁有不同的權限。我們可以使用條件型別來定義一個型別別名,根據使用者的角色動態選擇相應的權限型別。 ### 定義基本型別和條件型別別名 ```tsx // 基本的使用者和管理員權限型別 type UserPermissions = { canView: boolean; canEdit: boolean; }; type AdminPermissions = { canView: boolean; canEdit: boolean; canDelete: boolean; canCreateUser: boolean; }; // 角色型別 type UserRole = 'user' | 'admin'; // 條件型別別名 // 如果 UserRole 賦值也是 `admin` 字串,那就是前者的型別 AdminPermissions // 反之則是 UserPermissions type Permissions = UserRole extends 'admin' ? AdminPermissions : UserPermissions; ``` ### 範例程式碼 ```tsx= function getPermissions(role: UserRole): Permissions { if (role === 'admin') { return { canView: true, canEdit: true, canDelete: true, canCreateUser: true }; } else { return { canView: true, canEdit: false }; } } // 測試不同角色的權限 const adminPermissions = getPermissions('admin'); const userPermissions = getPermissions('user'); console.log('管理員權限:', adminPermissions); console.log('一般使用者權限:', userPermissions); ``` **這種方法提供了蠻大的靈活性,允許在不同情境下可以重用相同的邏輯,而不需要重寫型別判斷。這就是條件型別的優勢所在**,很適合用在需要根據某些條件(例如配置選項、用戶角色、環境設定等) ## 單選題 1. 在 TypeScript 中,型別別名(Type Aliases)的使用是為了什麼目的? A. 為了讓變數名稱更短 B. 為了創建新的型別 C. 為了給已存在的型別一個新名稱,並可用於建立複雜型別結構 D. 僅用於數字和字符串型別 2. 條件型別(Conditional Types)的一般形式是什麼? A. A extends B ? C : D B. A && B ? C : D C. A ? B : C D. A || B ? C : D 3. 下面哪個型別別名,是宣告為原始資料型別? A. type Example = 1; B. type Example = 'string'; C. type Example = string; ## **實作題** ### 圖書館借閱管理系統 你正在開發一個圖書館的借閱管理系統,希望能夠控制維護圖書館的會員、書籍以及借閱的記錄,為了先快速交辦一個初版的系統,你決定先針對這幾個核心部分進行設計,暫時不考慮借閱和還書的日期來簡化整體的設計。 #### 任務描述 1. **定義介面** - **`Member`**:包含會員姓名(**`name`**)、會員編號(**`memberId`**)、Email(**`email`**)。 - **`Book`**:包含書名(**`title`**)、作者(**`author`**)、ISBN(**`isbn`**,字串型別)與是否可借(**`available`**,布林型別)。 - **`Loan`**:包含會員編號(**`memberId`**)、ISBN(**`isbn`**)。 2. **實作資料結構** - 建立 **`members`** 陣列儲存 **`Member`**。 - 建立 **`books`** 陣列儲存 **`Book`**。 - 建立 **`loans`** 陣列儲存 **`Loan`**。 3. **開發功能** - **`addMember`**:新增會員到 **`members`**。 - **`addBook`**:新增書籍到 **`books`**。 - **`checkoutBook`**:會員借書(需檢查會員存在、書存在且可借、未被同會員重複借出)。 - **`returnBook`**:會員還書(需檢查借閱紀錄存在、還書後書籍 available 應設回 true,並移除該筆 Loan)。 - **`listMemberLoans`**:列出指定會員所有借閱紀錄。 #### 部分程式碼 ```tsx // TODO: 定義 Member 型別 type Member // TODO: 定義 Book 介面 type Book // TODO: 定義 Loan 介面 type Loan // 資料結構 let members: Member[] = []; let books: Book[] = []; let loans: Loan[] = []; // 功能實作 // 新增會員 function addMember(member: Member): void { // TODO: 可加上檢查 memberId 不重複 members.push(member); console.log(`會員已新增:${member.name} (${member.memberId})`); } // 新增書籍 function addBook(book: Book): void { // TODO: 可加上檢查 isbn 不重複 books.push(book); console.log(`書籍已新增:${book.title} (${book.isbn})`); } // 會員借書 function checkoutBook(memberId: string, isbn: string): void { // 1) 檢查會員是否存在 const member = members.find(m => m.memberId === memberId); if (!member) { throw new Error(`找不到會員:${memberId}`); } // 2) 檢查書籍是否存在且可借 const book = books.find(b => b.isbn === isbn); if (!book) { throw new Error(`找不到書籍 ISBN:${isbn}`); } if (!book.available) { throw new Error(`書籍目前不可借出:${book.title}`); } // 3) TODO: 檢查是否已被同會員借出(避免重複借同一本) // 4) TODO: 新增借閱紀錄 & 設為不可借 console.log(`借書成功:${member.name} 借閱《${book.title}》`); } // 會員還書 function returnBook(memberId: string, isbn: string): void { // 1) 找到對應借閱紀錄索引 const idx = loans.findIndex(l => l.memberId === memberId && l.isbn === isbn); if (idx === -1) { throw new Error(`找不到借閱紀錄(memberId=${memberId}, isbn=${isbn})。`); } const loan = loans[idx]; // 2) TODO: 刪除借閱紀錄 // 3) TODO: 找到書籍並將書籍設為可借 console.log(`還書成功:ISBN=${isbn},會員=${memberId}`); } // 測試操作 addMember({ name: "Alice", memberId: "M001", email: "alice@example.com" }); addBook({ title: "Clean Code", author: "Robert C. Martin", isbn: "978-0132350884", available: true }); addBook({ title: "The Pragmatic Programmer", author: "Andrew Hunt", isbn: "978-0201616224", available: true }); checkoutBook("M001", "978-0132350884"); returnBook("M001", "978-0132350884"); // 再借另一本 checkoutBook("M001", "978-0201616224"); ``` <!-- 解答 單選題:C、A、C 實作題:https://codepen.io/zeoxer/pen/XJXwVPL -->