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