第 8 課:泛型 (Generics)
學習目標
- 理解泛型嘅概念以及佢點樣提高程式碼嘅靈活性同可重用性
- 掌握如何定義同使用泛型函式、泛型介面同泛型類別
- 學識使用泛型約束嚟限制類型參數嘅範圍
- 了解點樣喺泛型約束中引用其他類型參數
- 初步認識點樣喺泛型中處理類別類型
1. 乜嘢係泛型 (What are Generics?)
泛型 (Generics) 允許我哋編寫能夠適用於多種唔同類型嘅組件 (例如函式、類別、介面),而唔係為每種類型寫一份幾乎相同嘅程式碼。佢哋提供咗一種方式,可以將類型作為參數傳遞俾組件,從而喺保持類型安全嘅同時,提高程式碼嘅靈活性同重用性。
1.1 點解需要泛型?
- 程式碼重用: 避免為唔同類型編寫重複嘅邏輯。例如,一個函式如果只係想返回傳入嘅任何類型嘅值,冇泛型嘅話可能要用
any,但會失去類型信息。 - 類型安全: 相比使用
any,泛型可以喺編譯時期捕獲類型錯誤,並保留傳入同傳出值之間嘅類型關係。
1.2 泛型嘅基本語法 (<T>)
類型參數 (Type Parameter),通常用單個大階字母表示,例如 <T> (代表 Type)。呢個 <T> 喺定義泛型組件時作為一個佔位符,喺使用該組件時會被具體類型取代。
範例:一個簡單嘅 identity 函式
1// JavaScript - 動態類型,執行時才知道問題
2function identityAny(arg: any): any {
3 return arg;
4}
5let outputAny = identityAny("myString"); // outputAny 類型係 any
6let outputNumAny = identityAny(100); // outputNumAny 類型係 any
7
8// TypeScript 使用泛型 - 靜態類型,寫程式碼時就發現問題
9function identity<T>(arg: T): T { // T 係類型參數
10 return arg;
11}
12
13let outputString = identity<string>("myString"); // 明確指定 T 為 string
14console.log(outputString.toUpperCase()); // OK,outputString 類型係 string
15
16let outputNumber = identity<number>(100); // 明確指定 T 為 number
17console.log(outputNumber.toFixed(2)); // OK,outputNumber 類型係 number
18
19// 類型推斷 (更常見)
20let inferredString = identity("anotherString"); // TypeScript 會推斷 T 為 string
21console.log(inferredString.length);
22
23let inferredBoolean = identity(true); // TypeScript 會推斷 T 為 boolean
24console.log(inferredBoolean);程式碼解釋:
上面嘅例子展示咗 identityAny 函式使用 any 類型,雖然可以接受任何類型嘅參數,但同時亦都失去咗參數同返回值之間嘅類型關聯。
相反,identity<T> 函式使用咗泛型。類型參數 T 喺呢度扮演一個佔位符嘅角色。當我哋調用 identity<string>("myString") 時,T 就會被 string 取代。因此,outputString 嘅類型被正確推斷為 string,我哋可以安全咁調用 toUpperCase() 方法。
🎯 互動練習 1:體驗你嘅第一個泛型函式
2. 泛型函式 (Generic Functions)
泛型函式係指可以操作多種類型數據嘅函式,而唔係單一固定類型。我哋可以定義一個或多個類型參數。
範例:
1// 交換兩個變數嘅值,呢兩個變數可以係唔同類型
2function swap<T, U>(tuple: [T, U]): [U, T] {
3 return [tuple[1], tuple[0]];
4}
5
6let swapped = swap<string, number>(["hello", 123]);
7console.log(swapped); // 輸出: [123, "hello"] - 類型係 [number, string]
8console.log(swapped[1].toUpperCase()); // 輸出: "HELLO"
9
10// 泛型函式處理陣列,獲取陣列長度
11function getArrayLength<T>(arr: T[]): number {
12 return arr.length;
13}
14console.log(getArrayLength<number>([1, 2, 3])); // 輸出: 3
15console.log(getArrayLength(["a", "b", "c", "d"])); // 輸出: 4 (類型推斷 T 為 string)
16
17// 箭頭函式中使用泛型,獲取陣列最後一個元素
18const getLastElement = <T>(arr: T[]): T | undefined => {
19 return arr.length > 0 ? arr[arr.length - 1] : undefined;
20};🎯 互動練習 2:實作泛型陣列處理函式
1// 正確答案
2function mapArray<T, U>(arr: T[], mappingFn: (item: T) => U): U[] {
3 let result: U[] = [];
4 for (let item of arr) {
5 result.push(mappingFn(item));
6 }
7 return result;
8}
9
10function filterArray<T>(arr: T[], predicate: (item: T) => boolean): T[] {
11 let result: T[] = [];
12 for (let item of arr) {
13 if (predicate(item)) {
14 result.push(item);
15 }
16 }
17 return result;
18}
19
20// 測試你嘅函式(請勿修改下面嘅測試程式碼)
21let numbers = [1, 2, 3, 4];
22let mappedResult = mapArray(numbers, (num) => `Number: ${num}`);
23console.log("映射結果:", mappedResult.join(","));
24
25let evenNumbers = filterArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], (num) => num % 2 === 0);
26console.log("過濾結果:", evenNumbers.join(","));3. 泛型介面 (Generic Interfaces)
介面亦可以係泛型嘅,允許我哋創建可以適用於唔同類型嘅介面定義。呢啲泛型介面可以描述唔同類型嘅物件結構或者函式簽名。
範例:
1// 描述一個鍵值對嘅泛型介面
2interface Pair<K, V> {
3 key: K;
4 value: V;
5}
6
7let stringNumberPair: Pair<string, number> = { key: "age", value: 30 };
8let numberBooleanPair: Pair<number, boolean> = { key: 1, value: true };
9
10console.log(stringNumberPair); // 輸出: { key: 'age', value: 30 }
11console.log(numberBooleanPair); // 輸出: { key: 1, value: true }
12
13// 描述一個泛型函式簽名嘅介面
14interface GenericIdentityFn<T> {
15 (arg: T): T;
16}
17
18function identityForInterface<T>(arg: T): T {
19 return arg;
20}
21
22let myIdentity: GenericIdentityFn<number> = identityForInterface;
23console.log(myIdentity(101)); // 輸出: 101
24
25// 描述一個包含內容嘅盒子嘅泛型介面
26interface Box<T> {
27 contents: T;
28 updateContents(newContents: T): void;
29}
30
31const stringBox: Box<string> = {
32 contents: "Hello TypeScript",
33 updateContents(newContents: string) {
34 this.contents = newContents;
35 console.log(`Box updated to: ${this.contents}`);
36 }
37};🎯 互動練習 3:建立泛型快取系統
1// 正確答案
2interface Cache<T> {
3 get(key: string): T | undefined;
4 set(key: string, value: T): void;
5}
6
7class SimpleCache<T> implements Cache<T> {
8 private data: { [key: string]: T } = {};
9
10 get(key: string): T | undefined {
11 return this.data[key];
12 }
13
14 set(key: string, value: T): void {
15 this.data[key] = value;
16 }
17}
18
19// 測試你嘅實作(請勿修改下面嘅測試程式碼)
20let stringCache = new SimpleCache<string>();
21let numberCache = new SimpleCache<number>();
22
23stringCache.set("greeting", "Hello World");
24numberCache.set("answer", 42);
25
26console.log("字串快取:", stringCache.get("greeting"));
27console.log("數字快取:", numberCache.get("answer"));
28
29stringCache.set("greeting", "TypeScript is great!");
30numberCache.set("answer", 100);
31
32console.log("字串快取 (更新後):", stringCache.get("greeting"));
33console.log("數字快取 (更新後):", numberCache.get("answer"));4. 泛型類別 (Generic Classes)
類別亦可以係泛型嘅。泛型類別嘅類型參數喺實例化類別時指定,令到類別嘅屬性同方法可以基於呢個類型參數工作。
範例:
1class DataStorage<T> {
2 private data: T[] = [];
3
4 addItem(item: T): void {
5 this.data.push(item);
6 }
7
8 getItem(index: number): T | undefined {
9 if (index >= 0 && index < this.data.length) {
10 return this.data[index];
11 }
12 return undefined;
13 }
14
15 getAllItems(): T[] {
16 return [...this.data]; // 返回一個新嘅陣列副本,避免外部修改
17 }
18}
19
20// 儲存字串嘅 DataStorage
21const stringStorage = new DataStorage<string>();
22stringStorage.addItem("Apple");
23stringStorage.addItem("Banana");
24console.log(stringStorage.getItem(0)); // 輸出: "Apple"
25
26// 儲存數字嘅 DataStorage
27const numberStorage = new DataStorage<number>();
28numberStorage.addItem(10);
29numberStorage.addItem(20);
30console.log(numberStorage.getAllItems()); // 輸出: [10, 20]5. 泛型約束 (Generic Constraints - extends)
有時我哋希望泛型函式或類別只能處理具有特定屬性或方法嘅類型。泛型約束允許我哋限制類型參數 T 必須符合某個介面或特定結構。使用 extends 關鍵字嚟添加約束。
範例:
1// 定義一個介面,要求物件必須有 length 屬性
2interface Lengthwise {
3 length: number;
4}
5
6// T 必須符合 Lengthwise 介面嘅要求
7function loggingIdentity<T extends Lengthwise>(arg: T): T {
8 console.log(`Length: ${arg.length}`); // 依家可以安全存取 arg.length
9 return arg;
10}
11
12loggingIdentity("hello"); // OK,string 有 length 屬性
13loggingIdentity([1, 2, 3]); // OK,array 有 length 屬性
14loggingIdentity({ length: 10, value: 3 }); // OK,物件有 length 屬性
15
16// 約束類型參數為特定物件嘅鍵 (keyof)
17function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
18 return obj[key];
19}
20
21let x = { a: 1, b: "hello", c: true };
22let valA = getProperty(x, "a"); // valA 類型係 number
23let valB = getProperty(x, "b"); // valB 類型係 string🎯 互動練習 5:使用泛型約束
1// 正確答案
2function printName<T extends { name: string }>(obj: T): void {
3 console.log(`${obj.name} 嘅名字長度: ${obj.name.length}`);
4}
5
6function getObjectProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
7 return obj[key];
8}
9
10// 測試你嘅函式(請勿修改下面嘅測試程式碼)
11let user1 = { name: "Alice", age: 30, id: 1 };
12let user2 = { name: "Bob", role: "admin" };
13
14printName(user1);
15printName(user2);
16
17console.log("用戶 ID:", getObjectProperty(user1, "id"));
18console.log("用戶名字:", getObjectProperty(user1, "name"));6. 喺泛型約束中使用類型參數
一個類型參數可以被另一個類型參數約束。呢個通常用於確保唔同參數之間存在特定嘅類型關係。
範例:
1function assignProperty<Obj, Key extends keyof Obj, Value extends Obj[Key]>(
2 obj: Obj,
3 key: Key,
4 value: Value
5): void {
6 obj[key] = value; // 類型安全:value 嘅類型保證同 obj[key] 嘅類型兼容
7}
8
9const myObj = { id: 1, name: "Default", active: false };
10
11assignProperty(myObj, "name", "New Name"); // OK
12assignProperty(myObj, "active", true); // OK
13// assignProperty(myObj, "id", "not a number"); // 錯誤:類型不匹配
14
15console.log(myObj); // { id: 1, name: 'New Name', active: true }7. 喺泛型中使用類別類型
當你使用泛型創建工廠函式時,可能需要引用類別嘅建構函式類型。
範例:
1// 工廠函式:創建唔同類型嘅物件
2function createInstance<T>(c: { new (): T }): T {
3 return new c();
4}
5
6class BeeKeeper {
7 hasMask: boolean = true;
8 constructor() {
9 console.log("BeeKeeper instance created");
10 }
11}
12
13class ZooKeeper {
14 nametag: string = "ZooKpr";
15 constructor() {
16 console.log("ZooKeeper instance created");
17 }
18}
19
20let keeper1 = createInstance(BeeKeeper);
21let keeper2 = createInstance(ZooKeeper);
22
23console.log(keeper1.hasMask); // true
24console.log(keeper2.nametag); // "ZooKpr"🎯 互動練習 6:泛型工廠函式
1// TODO: 實作 createInstanceWithArgs 函式
2function createInstanceWithArgs<A extends any[], T>(
3 constructor: new (...args: A) => T,
4 ...args: A
5): T {
6 // 請實作呢個函式,使用建構函式同參數創建實例
7 return undefined as any; // 請修改呢行
8}
9
10class Product {
11 name: string;
12 price: number;
13
14 constructor(name: string, price: number) {
15 this.name = name;
16 this.price = price;
17 console.log(`Product "${name}" created with price ${price}`);
18 }
19
20 getInfo(): string {
21 return `${this.name} - $${this.price}`;
22 }
23}
24
25// 測試你嘅工廠函式(請勿修改下面嘅測試程式碼)
26const laptop = createInstanceWithArgs(Product, "Laptop", 1200);
27const book = createInstanceWithArgs(Product, "Book", 25);
28
29console.log(laptop.getInfo());
30console.log(book.getInfo());1// 正確答案
2function createInstanceWithArgs<A extends any[], T>(
3 constructor: new (...args: A) => T,
4 ...args: A
5): T {
6 return new constructor(...args);
7}
8
9class Product {
10 name: string;
11 price: number;
12
13 constructor(name: string, price: number) {
14 this.name = name;
15 this.price = price;
16 console.log(`Product "${name}" created with price ${price}`);
17 }
18
19 getInfo(): string {
20 return `${this.name} - $${this.price}`;
21 }
22}
23
24// 測試你嘅工廠函式(請勿修改下面嘅測試程式碼)
25const laptop = createInstanceWithArgs(Product, "Laptop", 1200);
26const book = createInstanceWithArgs(Product, "Book", 25);
27
28console.log(laptop.getInfo());
29console.log(book.getInfo());8. 綜合練習
🎯 互動練習 7:建立泛型資料管理系統
1// 正確答案
2interface Identifiable {
3 id: number;
4}
5
6class Repository<T extends Identifiable> {
7 private items: T[] = [];
8
9 add(item: T): void {
10 this.items.push(item);
11 }
12
13 findById(id: number): T | undefined {
14 return this.items.find(item => item.id === id);
15 }
16
17 getAll(): T[] {
18 return [...this.items];
19 }
20
21 count(): number {
22 return this.items.length;
23 }
24}
25
26// 測試用嘅類別(請勿修改)
27class User implements Identifiable {
28 constructor(public id: number, public name: string) {}
29}
30
31class Product implements Identifiable {
32 constructor(public id: number, public name: string) {}
33}
34
35// 測試你嘅實作(請勿修改下面嘅測試程式碼)
36let userRepo = new Repository<User>();
37let productRepo = new Repository<Product>();
38
39let user1 = new User(1, "Alice");
40let user2 = new User(2, "Bob");
41let product1 = new Product(101, "Laptop");
42let product2 = new Product(102, "Mouse");
43
44userRepo.add(user1);
45console.log("添加用戶: Alice (ID: 1)");
46userRepo.add(user2);
47console.log("添加用戶: Bob (ID: 2)");
48
49productRepo.add(product1);
50console.log("添加產品: Laptop (ID: 101)");
51productRepo.add(product2);
52console.log("添加產品: Mouse (ID: 102)");
53
54let foundUser = userRepo.findById(1);
55console.log("找到用戶:", foundUser?.name);
56
57let foundProduct = productRepo.findById(101);
58console.log("找到產品:", foundProduct?.name);
59
60console.log("用戶總數:", userRepo.count());
61console.log("產品總數:", productRepo.count());總結
今日學到嘅重點
- 泛型概念:使用
<T>創建可重用嘅組件,保持類型安全 - 泛型函式:可以處理多種類型嘅函式,支持類型推斷
- 泛型介面:定義可適用於唔同類型嘅介面結構
- 泛型類別:創建可儲存同處理任意類型數據嘅類別
- 泛型約束:使用
extends限制類型參數嘅範圍 - 類型參數關係:一個類型參數可以約束另一個類型參數
- 類別類型:喺泛型中使用建構函式類型
課後建議
- 練習編寫更多泛型函式,體驗類型推斷嘅便利
- 嘗試創建自己嘅泛型類別,例如隊列 (Queue) 或樹 (Tree)
- 學習使用泛型約束解決實際問題
- 觀察 TypeScript 內建類型(如 Array、Promise)如何使用泛型
下一課預告
下一課我哋會學習:
- 聯合類型同交叉類型
- 類型守衛同類型收窄
- 字面量類型
- 條件類型同映射類型
恭喜你完成第八課! 🎉
泛型係 TypeScript 嘅強大功能,掌握佢可以讓你寫出更靈活、更安全嘅程式碼。繼續練習,你會發現泛型喺實際開發中嘅巨大價值!