第 9 課:進階類型 (Advanced Types)
學習目標
- 理解並能夠使用聯合類型嚟表示一個值可以係多種可能性之一
- 掌握交叉類型,將多個類型合併為一個
- 學識使用唔同嘅類型守衛 (typeof, instanceof, in, 自訂) 嚟進行類型收窄
- 理解字面量類型嘅概念同應用場景
- 掌握處理
null同undefined嘅技巧,包括可選串連同空值合併 - 學識使用類型別名簡化複雜類型定義同提高可讀性
1. 聯合類型 (Union Types - |)
聯合類型允許一個變數或參數可以係多個指定類型中嘅任何一個。我哋會用 | (管道符號) 嚟分隔唔同嘅類型。就好似話:「呢個變數可以係數字,又或者可以係字串。」
範例:
typescript
1function printId(id: number | string): void {
2 console.log("你嘅 ID 係: " + id);
3}
4
5printId(101); // 你嘅 ID 係: 101
6printId("202ABC"); // 你嘅 ID 係: 202ABC
7
8// 處理聯合類型 (類型收窄)
9function processInput(input: string | number | boolean): void {
10 if (typeof input === "string") {
11 // 喺呢個 if 區塊入面,TypeScript 知道 input 肯定係字串
12 console.log("輸入係字串:", input.toUpperCase());
13 } else if (typeof input === "number") {
14 // 喺呢個 else if 區塊入面,TypeScript 知道 input 肯定係數字
15 console.log("輸入係數字:", input.toFixed(2));
16 } else {
17 // 喺呢度,TypeScript 知道 input 肯定係布林值
18 console.log("輸入係布林值:", !input);
19 }
20}
21
22// 聯合類型與陣列
23let mixedArray: (string | number)[] = ["apple", 1, "banana", 2, "orange", 3];
24
25// 函式返回值
26function getLength(value: string | string[]): number {
27 if (typeof value === "string") {
28 return value.length;
29 } else {
30 return value.length; // 陣列都有 length 屬性
31 }
32}程式碼解釋: 當我哋有一個聯合類型嘅值,我哋通常需要先檢查佢嘅實際類型,然後先可以安全地使用該特定類型嘅方法或屬性,呢個過程就叫做「類型收窄」(Type Narrowing)。
🎯 互動練習 1:聯合類型處理
💡 參考答案
typescript
1// 正確答案
2function displayValue(value: string | string[] | null): void {
3 if (value === null) {
4 console.log("空值: 沒有提供值");
5 } else if (typeof value === "string") {
6 console.log("字串值:", value.toUpperCase());
7 } else {
8 console.log("陣列值:", value.join(","));
9 }
10}
11
12// 測試你嘅函式(請勿修改下面嘅測試程式碼)
13displayValue("hello");
14displayValue(["apple", "banana", "cherry"]);
15displayValue(null);2. 交叉類型 (Intersection Types - &)
交叉類型允許我哋將多個現有類型合併成一個新類型,呢個新類型將擁有所有原始類型嘅所有成員。我哋會用 & (與符號) 嚟分隔唔同嘅類型。
範例:
typescript
1interface Draggable {
2 drag(): void;
3}
4
5interface Resizable {
6 resize(): void;
7}
8
9// UIElement 類型必須同時實現 Draggable 同 Resizable 介面嘅所有成員
10type UIElement = Draggable & Resizable;
11
12let interactiveBox: UIElement = {
13 drag: () => console.log("拖曳緊個盒..."),
14 resize: () => console.log("調整緊個盒嘅大細...")
15};
16
17// 合併物件屬性
18interface Person {
19 name: string;
20 age: number;
21}
22
23interface Employee {
24 employeeId: string;
25 department: string;
26}
27
28// EmployedPerson 類型將會擁有 Person 同 Employee 嘅所有屬性
29type EmployedPerson = Person & Employee;
30
31const john: EmployedPerson = {
32 name: "John Doe",
33 age: 30,
34 employeeId: "E123",
35 department: "工程部"
36};🎯 互動練習 2:交叉類型組合
💡 參考答案
typescript
1// 正確答案
2interface HasColor {
3 color: string;
4}
5
6interface HasArea {
7 getArea(): number;
8}
9
10type ColoredShape = HasColor & HasArea;
11
12const redCircle: ColoredShape = {
13 color: "紅色",
14 getArea(): number {
15 const radius = 5;
16 return Math.PI * radius * radius;
17 }
18};
19
20// 測試你嘅實作(請勿修改下面嘅測試程式碼)
21console.log("顏色:", redCircle.color);
22console.log("面積:", redCircle.getArea().toFixed(2));3. 類型守衛 (Type Guards)
類型守衛係一種喺執行時期檢查變數類型嘅表達式。當 TypeScript 編譯器遇到類型守衛,並且守衛條件為真嘅時候,佢可以喺嗰個條件區塊內部將變數嘅類型「收窄」到一個更具體嘅類型。
3.1 typeof 類型守衛
typeof 運算子可以檢查一個變數嘅基本類型。
範例:
typescript
1function padLeft(value: string, padding: string | number) {
2 if (typeof padding === "number") {
3 // 喺呢個區塊,TypeScript 知道 padding 係 number
4 return Array(padding + 1).join(" ") + value;
5 }
6 if (typeof padding === "string") {
7 // 喺呢個區塊,TypeScript 知道 padding 係 string
8 return padding + value;
9 }
10 throw new Error(`預期係字串或數字,但收到 '${typeof padding}'.`);
11}3.2 instanceof 類型守衛
instanceof 運算子用於檢查一個物件是否係某個類別嘅實例。
範例:
typescript
1class Bird {
2 fly() {
3 console.log("雀仔飛緊...");
4 }
5 layEggs() {
6 console.log("雀仔生緊蛋...");
7 }
8}
9
10class Fish {
11 swim() {
12 console.log("魚游緊水...");
13 }
14 layEggs() {
15 console.log("魚生緊蛋...");
16 }
17}
18
19function getPetMove(pet: Bird | Fish) {
20 pet.layEggs(); // 兩個類別都有 layEggs 方法
21
22 if (pet instanceof Bird) {
23 // 喺呢個區塊,TypeScript 知道 pet 係 Bird 嘅實例
24 pet.fly();
25 } else {
26 // 喺呢個區塊,TypeScript 知道 pet 係 Fish 嘅實例
27 pet.swim();
28 }
29}3.3 in 類型守衛
in 運算子用於檢查一個物件是否有特定嘅屬性。
範例:
typescript
1interface Car {
2 drive(): void;
3 wheels: number;
4}
5
6interface Boat {
7 sail(): void;
8 propeller: boolean;
9}
10
11function operateVehicle(vehicle: Car | Boat) {
12 if ("wheels" in vehicle) {
13 // 喺呢個區塊,TypeScript 知道 vehicle 係 Car
14 vehicle.drive();
15 console.log(`車有 ${vehicle.wheels} 個輪`);
16 } else {
17 // 喺呢個區塊,TypeScript 知道 vehicle 係 Boat
18 vehicle.sail();
19 console.log(`船有螺旋槳: ${vehicle.propeller}`);
20 }
21}🎯 互動練習 3:類型守衛實作
💡 參考答案
typescript
1// 正確答案
2function moveAnimal(animal: Bird | Fish): string {
3 if (animal instanceof Bird) {
4 return animal.fly();
5 } else {
6 return animal.swim();
7 }
8}
9
10function operateVehicle(vehicle: Car | Boat): string {
11 if ("wheels" in vehicle) {
12 return vehicle.drive();
13 } else {
14 return vehicle.sail();
15 }
16}
17
18// 動物類別(請勿修改)
19class Bird {
20 fly() { return "雀仔飛緊..."; }
21}
22
23class Fish {
24 swim() { return "魚游緊水..."; }
25}
26
27// 車輛介面(請勿修改)
28interface Car {
29 drive(): string;
30 wheels: number;
31}
32
33interface Boat {
34 sail(): string;
35 propeller: boolean;
36}
37
38// 測試你嘅函式(請勿修改下面嘅測試程式碼)
39let bird = new Bird();
40let fish = new Fish();
41let car: Car = { drive: () => "開車中...", wheels: 4 };
42let boat: Boat = { sail: () => "航行中...", propeller: true };
43
44console.log("動物移動:", moveAnimal(bird));
45console.log("動物移動:", moveAnimal(fish));
46console.log("車輛操作:", operateVehicle(car));
47console.log("車輛操作:", operateVehicle(boat));4. 字面量類型 (Literal Types)
字面量類型允許我哋指定一個變數只能係特定嘅值,而唔係整個類型嘅任何值。
範例:
typescript
1// 字串字面量類型
2type Direction = "up" | "down" | "left" | "right";
3
4function move(direction: Direction) {
5 console.log(`移動方向: ${direction}`);
6}
7
8move("up"); // OK
9move("down"); // OK
10// move("diagonal"); // 錯誤:不是允許嘅值
11
12// 數字字面量類型
13type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
14
15function rollDice(): DiceRoll {
16 return Math.floor(Math.random() * 6) + 1 as DiceRoll;
17}
18
19// 布林字面量類型
20type Success = true;
21type Failure = false;
22
23// 混合字面量類型
24type Status = "loading" | "success" | "error" | 404 | 500;
25
26function handleStatus(status: Status) {
27 switch (status) {
28 case "loading":
29 console.log("載入中...");
30 break;
31 case "success":
32 console.log("成功!");
33 break;
34 case "error":
35 console.log("發生錯誤");
36 break;
37 case 404:
38 console.log("找不到頁面");
39 break;
40 case 500:
41 console.log("伺服器錯誤");
42 break;
43 }
44}🎯 互動練習 4:字面量類型應用
💡 參考答案
typescript
1// 正確答案
2type Theme = "light" | "dark" | "high-contrast";
3
4function applyTheme(theme: Theme): string {
5 switch (theme) {
6 case "light":
7 return "淺色模式";
8 case "dark":
9 return "深色模式";
10 case "high-contrast":
11 return "高對比模式";
12 }
13}
14
15type ButtonSize = "small" | "medium" | "large";
16
17function createButton(size: ButtonSize, theme: Theme): string {
18 const themeDesc = applyTheme(theme);
19 let sizeDesc = "";
20
21 switch (size) {
22 case "small":
23 sizeDesc = "小";
24 break;
25 case "medium":
26 sizeDesc = "中";
27 break;
28 case "large":
29 sizeDesc = "大";
30 break;
31 }
32
33 return `${sizeDesc}按鈕 (${themeDesc})`;
34}
35
36// 測試你嘅函式(請勿修改下面嘅測試程式碼)
37console.log("當前主題:", applyTheme("light"));
38console.log("當前主題:", applyTheme("dark"));
39console.log("當前主題:", applyTheme("high-contrast"));5. 處理 null 同 undefined
TypeScript 提供咗多種方式嚟安全地處理可能為空嘅值。
5.1 可選串連 (Optional Chaining - ?.)
typescript
1interface User {
2 name: string;
3 address?: {
4 street: string;
5 city: string;
6 };
7}
8
9function getUserCity(user: User): string | undefined {
10 // 安全地存取嵌套屬性
11 return user.address?.city;
12}
13
14// 可選方法調用
15interface API {
16 getData?(): string;
17}
18
19function callAPI(api: API) {
20 // 只有當 getData 方法存在時先調用
21 const data = api.getData?.();
22 return data;
23}5.2 空值合併 (Nullish Coalescing - ??)
typescript
1function getDisplayName(name: string | null | undefined): string {
2 // 只有當 name 係 null 或 undefined 時先使用預設值
3 return name ?? "匿名用戶";
4}
5
6// 與 || 嘅區別
7function example(value: string | null | undefined | "" | 0 | false) {
8 console.log(value || "預設值"); // 所有 falsy 值都會使用預設值
9 console.log(value ?? "預設值"); // 只有 null/undefined 會使用預設值
10}🎯 互動練習 5:安全處理空值
💡 參考答案
typescript
1// 正確答案
2function getUserInfo(user: User | null): string {
3 const userName = user?.name ?? "匿名用戶";
4 const city = user?.profile?.address?.city ?? "未知";
5
6 return `用戶: ${userName}, 城市: ${city}`;
7}
8
9interface User {
10 name?: string;
11 profile?: {
12 address?: {
13 city?: string;
14 };
15 };
16}
17
18// 測試你嘅函式(請勿修改下面嘅測試程式碼)
19const user1: User = {
20 name: "Alice",
21 profile: {
22 address: {
23 city: "香港"
24 }
25 }
26};
27
28const user2: User = {
29 name: "Bob",
30 profile: {}
31};
32
33const user3: User = {};
34
35console.log(getUserInfo(user1));
36console.log(getUserInfo(user2));
37console.log(getUserInfo(user3));6. 類型別名 (Type Aliases)
類型別名允許我哋為複雜嘅類型創建一個簡短嘅名稱,提高程式碼嘅可讀性同可維護性。
範例:
typescript
1// 基本類型別名
2type ID = string | number;
3type UserRole = "admin" | "user" | "guest";
4
5// 複雜物件類型別名
6type User = {
7 id: ID;
8 name: string;
9 role: UserRole;
10 isActive: boolean;
11};
12
13// 函式類型別名
14type EventHandler = (event: string) => void;
15type Validator<T> = (value: T) => boolean;
16
17// 條件類型別名
18type NonNullable<T> = T extends null | undefined ? never : T;
19
20// 使用類型別名
21function createUser(id: ID, name: string, role: UserRole): User {
22 return {
23 id,
24 name,
25 role,
26 isActive: true
27 };
28}
29
30const stringValidator: Validator<string> = (value) => value.length > 0;
31const numberValidator: Validator<number> = (value) => value > 0;🎯 互動練習 6:綜合練習 - 電商系統
💡 參考答案
typescript
1// 正確答案
2type ProductStatus = "in-stock" | "out-of-stock" | "discontinued";
3type OrderStatus = "pending" | "processing" | "shipped" | "delivered";
4
5interface Product {
6 id: number;
7 name: string;
8 price: number;
9 status: ProductStatus;
10}
11
12interface Order {
13 id: number;
14 products: Product[];
15 status: OrderStatus;
16}
17
18function getProductInfo(product: Product): string {
19 let statusText = "";
20
21 switch (product.status) {
22 case "in-stock":
23 statusText = "有庫存";
24 break;
25 case "out-of-stock":
26 statusText = "缺貨";
27 break;
28 case "discontinued":
29 statusText = "已停產";
30 break;
31 }
32
33 return `產品: ${product.name} - $${product.price} (${statusText})`;
34}
35
36function getOrderStatus(order: Order): string {
37 let statusText = "";
38
39 switch (order.status) {
40 case "pending":
41 statusText = "待處理";
42 break;
43 case "processing":
44 statusText = "處理中";
45 break;
46 case "shipped":
47 statusText = "已發貨";
48 break;
49 case "delivered":
50 statusText = "已送達";
51 break;
52 }
53
54 return `訂單狀態: ${statusText}`;
55}
56
57// 測試你嘅實作(請勿修改下面嘅測試程式碼)
58const laptop: Product = { id: 1, name: "Laptop", price: 1200, status: "in-stock" };
59const phone: Product = { id: 2, name: "Phone", price: 800, status: "out-of-stock" };
60
61const order1: Order = { id: 1, products: [laptop], status: "processing" };
62const order2: Order = { id: 2, products: [phone], status: "shipped" };
63
64console.log(getProductInfo(laptop));
65console.log(getProductInfo(phone));
66console.log(getOrderStatus(order1));
67console.log(getOrderStatus(order2));總結
今日學到嘅重點
- 聯合類型 (
|):表示一個值可以係多種類型之一 - 交叉類型 (
&):將多個類型合併為一個新類型 - 類型守衛:使用
typeof、instanceof、in進行類型收窄 - 字面量類型:限制變數只能係特定嘅值
- 空值處理:使用
?.同??安全處理 null/undefined - 類型別名:為複雜類型創建簡短嘅名稱
課後建議
- 練習使用聯合類型同交叉類型組合複雜嘅數據結構
- 熟練掌握各種類型守衛嘅使用場景
- 在實際專案中使用字面量類型提高程式碼安全性
- 善用可選串連同空值合併處理不確定嘅數據
下一課預告
下一課我哋會學習:
- 列舉 (Enums) 嘅使用
- 模組系統嘅基礎
- 匯入同匯出嘅語法
- 命名空間嘅概念
恭喜你完成第九課! 🎉
進階類型係 TypeScript 嘅核心功能,掌握呢啲概念可以讓你寫出更安全、更靈活嘅程式碼。繼續練習,你會發現呢啲類型工具喺實際開發中嘅強大威力!