# 🏅 Day 7 - 介面(interface)
### 介面 (interface)
在 TypeScript 裡頭,介面(Interface)和 type 的作用類似,能夠用來定義物件的型別。
### 基本範例
```tsx=
interface Student {
studentId: number;
name: string;
}
// 正確
let bob: Student = {
studentId: 101,
name: 'Bob'
};
// 錯誤,只可指定已知的屬性,且 'Student' 中沒有 'gender'。
let Tom: Student = {
studentId: 101,
name: 'Tom',
gender:'male' // 會報錯
};
```
這裡定義了一個 **`Student`** 介面,然後建立了一個符合這個介面的物件 **`bob`**。
TypeScript 要求物件必須跟介面的定義完全一致,物件多出或少了介面中的屬性都不行。
### 可選屬性
如果有些屬性是選擇性的,可以在屬性名稱後面加上一個問號 `?`:
```tsx
interface Student {
studentId: number;
name: string;
grade?: number;
}
// 正確
let alice: Student = {
studentId: 102,
name: 'Alice',
grade: 90
};
// 正確
let lily: Student = {
studentId: 103,
name: 'Lily'
};
```
### 任意屬性
如果想讓介面可以接受其他任何名稱的屬性,可以這樣寫:
```tsx
interface Student {
studentId: number;
name: string;
grade?: number;
[propName: string]: any;
}
let john: Student = {
studentId: 104,
name: 'John',
department: 'History'
};
```
用 **`[propName: string]`** 表示除了已定義的屬性外,介面還可以接受任何其他的額外屬性。
### 唯讀屬性
如果某些屬性在建立後就不想讓它改變,可以用 **`readonly`** :
```tsx
interface Student {
readonly studentId: number;
name: string;
}
let mike: Student = {
studentId: 105,
name: 'Mike'
};
// mike.studentId = 106; // 這行會報錯,因為 studentId 是唯讀的
```
像是上面 **`studentId`** 被設定為 **`readonly`**,就不能再更改。
## 常用情境
1. **定義物件的形狀(Shape)**:當你需要預先定義一個物件應該包含哪些屬性和方法時,介面是非常合適的選擇。特別是在大型項目或團隊協作中。
2. **強化函式參數的型別檢查**:當你需要函式接受帶有特定屬性的物件時,可以使用介面來定義這些屬性,來提高程式碼的可讀性和可維護性。
3. **定義複雜的資料結構**:當處理複雜的資料結構,例如從 API 取得的大型 JSON 物件時,介面可以幫助你管理和理解這些資料結構的形狀,不用每次都需要通靈。
## 情境一:定義物件的形狀(Shape)
### **範例情境:使用者資訊管理系統**
在這個系統需要處理使用者資訊,像是姓名、年齡和電子郵件地址。為了確保在整個應用程式中一致處理這些資訊,我們可以用介面來定義一個「使用者」的形狀。
### 定義「使用者」介面
```tsx
// 重點:定義使用者的形狀
interface User {
name: string;
age: number;
email: string;
}
function displayUser(user: User) {
console.log(`Name: ${user.name}, Age: ${user.age}, Email: ${user.email}`);
}
let user1: User = {
name: "陳大明",
age: 30,
email: "damingchen@example.com"
};
displayUser(user1);
```
透過這樣的方式確保在整個應用程式中,所有處理使用者資訊的地方都會遵循同一個結構和型別,藉此提高程式碼的一致性和可靠性。
## 情境二:強化函式參數的型別檢查
### **範例情境:商品庫存管理系統**
一個商品庫存管理系統。我們需要處理各種商品的庫存狀況,包括商品名稱、數量和價格。為了確保函式能夠接收正確格式的商品資訊,我們可以用介面來強化函式參數的型別檢查。
### 定義「商品」介面
```tsx=
interface Product {
id: string,
name: string,
quantity: number,
price: number
}
// 重點:參數在型別註釋,使用 Product 介面
function updateInventory(product: Product, additionalQuantity: number) {
product.quantity += additionalQuantity;
console.log(`更新 ${product.name}: ${product.quantity} 品項到庫存`);
}
let newProduct: Product = {
id: "A105",
name: "筆記型電腦",
quantity: 20,
price: 45000
};
// 新增特定產品庫存量
updateInventory(newProduct, 5);
```
透過使用介面來強化函式參數的型別檢查,確保函式只接受符合特定結構的物件,這樣一來可以減少錯誤,又能提高代碼的可靠性 :D
## 情境三:定義複雜的資料結構
### **範例情境:線上課程平台的課程資訊管理**
假設六角學院正在開發一個線上課程平台,需要處理各種課程的詳細資訊,包括課程名稱、講師資訊、課程內容和學生評價等。由於這些資料結構相對複雜,就適合用介面來定義這些結構。
### 定義相關介面
```tsx=
// 講師介面
interface Instructor {
name: string;
expertise: string[];
}
// 課程介面,重要,有發現到這裡用到了 Instructor 介面嗎?
interface Course {
title: string;
description: string;
instructor: Instructor;
rating: number;
reviews: string[];
}
// 顯示課程資訊
function displayCourseInfo(course: Course) {
console.log(`課程名稱: ${course.title}`);
console.log(`課程描述: ${course.description}`);
console.log(`講者名稱: ${course.instructor.name}`);
console.log(`課程評價: ${course.rating}/5`);
}
let programmingCourse: Course = {
title: "進階 TypeScript 程式設計",
description: "深入瞭解 TypeScript 的高階特性。",
instructor: {
name: "王小明",
expertise: ["TypeScript", "JavaScript", "軟體工程"]
},
rating: 4.5,
reviews: ["很實用的課程!", "深入淺出,易於理解。"]
};
displayCourseInfo(programmingCourse);
```
像是這範例中,**`Instructor`** 介面定義了講師的姓名 (**`name`**) 和其專長領域 (**`expertise`**)。當 **`Course`** 介面中引用 **`Instructor`** 介面時,就表明每個課程都會有一位講師,並且這位講師的資訊會遵循 **`Instructor`** 介面的結構。
### 這樣的好處是在
1. **模組化和重用性**:通過將 **`Instructor`** 作為一個獨立的介面,我們可以在很多地方重用這個介面。這種模組化方法讓程式碼更加清晰,並減少重複性。
2. **清晰的結構定義**:將講師的資訊封裝在自己的介面中,可以讓 **`Course`** 介面的結構更加清晰。
3. **易於維護和擴充**:如果未來講師的資訊需要添加新的屬性或修改,我們只需更改 **`Instructor`** 介面。這種設計讓整個系統更容易維護和擴充。
## 單選題
1. **在 TypeScript 中,interface 主要用於:**
A. 定義函數的返回值
B. 儲存資料
C. 定義物件的形狀
D. 計算數值
2. **在 TypeScript 的 interface 中,可選屬性是如何表示的?**
A. 使用前綴符號 **`?`**
B. 使用前綴符號 **`#`**
C. 使用後綴符號 **`!`**
D. 使用後綴符號 **`^`**
3. **TypeScript 的 interface 可以用於哪種情況?**
A. 定義一個變數的數值
B. 管理複雜的資料結構
C. 進行數學運算
D. 設定程式的背景顏色
## 實作題
### 醫院病患和預約管理系統
#### 任務描述
1. **定義介面**
- **`Patient`** 介面:應包含病患的姓名(**`name`**)、年齡(**`age`**)、性別(**`gender`**)和病歷號碼(**`medicalRecordNumber`**)。
- **`Appointment`** 介面:應包含預約的日期(**`date`**)、時間(**`time`**)、醫生姓名(**`doctorName`**)和病患病歷號碼(**`patientMedicalRecordNumber`**),用來關聯病患的病歷號碼。
2. **實作資料結構**
- 建立一個 **`patients`** 陣列,用於儲存 **`Patient`** 物件。
- 建立一個 **`appointments`** 陣列,用於儲存 **`Appointment`** 物件。
3. **開發功能**
- 實作函式 **`addPatient`** 來添加新病患到 **`patients`** 陣列。
- 實作函式 **`scheduleAppointment`** 來為存在的病患安排新的預約,並加入到 **`appointments`** 陣列。
- 實作函式 **`cancelAppointment`** 來取消現有的預約。
- 實作函式 **`listAppointments`** 來列出特定病患的所有預約。
#### 部分程式碼
```tsx
// 定義 Patient 病患介面
interface Patient {
name: string;
age: number;
gender: string;
medicalRecordNumber: string;
}
// 定義 Appointment 預約介面
interface Appointment {
date: string;
time: string;
doctorName: string;
patientMedicalRecordNumber: string;
}
// 病患資料陣列
let patients: Patient[] = [];
// 預約資料陣列
let appointments: Appointment[] = [];
// 實作函式 addPatient
function addPatient(patient: Patient): void {
patients.push(patient);
}
// 實作函式 scheduleAppointment
function scheduleAppointment(appointment: Appointment): void {
// 確保病患存在
const patientExists = patients.some(
(patient) => patient.medicalRecordNumber === appointment.patientMedicalRecordNumber
);
if (!patientExists) {
throw new Error("病患不存在");
}
// TODO: 增加一筆預約到預約列表
}
// TODO: 函式 cancelAppointment
// TODO: 實作函式 listAppointments
// 增加病患
addPatient({ name: "John Doe", age: 30, gender: "Male", medicalRecordNumber: "12345" });
// 安排預約
scheduleAppointment({ date: "2024-01-15", time: "10:00", doctorName: "Dr. Smith", patientMedicalRecordNumber: "12345" });
```
<!-- 解答:
單選題:C、A、B
實作題:https://codepen.io/zeoxer/pen/ByjeJmd
-->