TypeScript Learning

泛型 (Generics)

⏱️預計 52 分鐘
📖TypeScript 課程
🎯實戰導向

第 8 課:泛型 (Generics)

學習目標

  • 理解泛型嘅概念以及佢點樣提高程式碼嘅靈活性同可重用性
  • 掌握如何定義同使用泛型函式、泛型介面同泛型類別
  • 學識使用泛型約束嚟限制類型參數嘅範圍
  • 了解點樣喺泛型約束中引用其他類型參數
  • 初步認識點樣喺泛型中處理類別類型

1. 乜嘢係泛型 (What are Generics?)

泛型 (Generics) 允許我哋編寫能夠適用於多種唔同類型嘅組件 (例如函式、類別、介面),而唔係為每種類型寫一份幾乎相同嘅程式碼。佢哋提供咗一種方式,可以將類型作為參數傳遞俾組件,從而喺保持類型安全嘅同時,提高程式碼嘅靈活性同重用性。

1.1 點解需要泛型?

  • 程式碼重用: 避免為唔同類型編寫重複嘅邏輯。例如,一個函式如果只係想返回傳入嘅任何類型嘅值,冇泛型嘅話可能要用 any,但會失去類型信息。
  • 類型安全: 相比使用 any,泛型可以喺編譯時期捕獲類型錯誤,並保留傳入同傳出值之間嘅類型關係。

1.2 泛型嘅基本語法 (<T>)

類型參數 (Type Parameter),通常用單個大階字母表示,例如 <T> (代表 Type)。呢個 <T> 喺定義泛型組件時作為一個佔位符,喺使用該組件時會被具體類型取代。

範例:一個簡單嘅 identity 函式

typescript
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)

泛型函式係指可以操作多種類型數據嘅函式,而唔係單一固定類型。我哋可以定義一個或多個類型參數。

範例:

typescript
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:實作泛型陣列處理函式

3. 泛型介面 (Generic Interfaces)

介面亦可以係泛型嘅,允許我哋創建可以適用於唔同類型嘅介面定義。呢啲泛型介面可以描述唔同類型嘅物件結構或者函式簽名。

範例:

typescript
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:建立泛型快取系統

4. 泛型類別 (Generic Classes)

類別亦可以係泛型嘅。泛型類別嘅類型參數喺實例化類別時指定,令到類別嘅屬性同方法可以基於呢個類型參數工作。

範例:

typescript
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 關鍵字嚟添加約束。

範例:

typescript
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:使用泛型約束

6. 喺泛型約束中使用類型參數

一個類型參數可以被另一個類型參數約束。呢個通常用於確保唔同參數之間存在特定嘅類型關係。

範例:

typescript
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. 喺泛型中使用類別類型

當你使用泛型創建工廠函式時,可能需要引用類別嘅建構函式類型。

範例:

typescript
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:泛型工廠函式

typescript
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());

8. 綜合練習

🎯 互動練習 7:建立泛型資料管理系統

總結

今日學到嘅重點

  • 泛型概念:使用 <T> 創建可重用嘅組件,保持類型安全
  • 泛型函式:可以處理多種類型嘅函式,支持類型推斷
  • 泛型介面:定義可適用於唔同類型嘅介面結構
  • 泛型類別:創建可儲存同處理任意類型數據嘅類別
  • 泛型約束:使用 extends 限制類型參數嘅範圍
  • 類型參數關係:一個類型參數可以約束另一個類型參數
  • 類別類型:喺泛型中使用建構函式類型

課後建議

  1. 練習編寫更多泛型函式,體驗類型推斷嘅便利
  2. 嘗試創建自己嘅泛型類別,例如隊列 (Queue) 或樹 (Tree)
  3. 學習使用泛型約束解決實際問題
  4. 觀察 TypeScript 內建類型(如 Array、Promise)如何使用泛型

下一課預告

下一課我哋會學習:

  • 聯合類型同交叉類型
  • 類型守衛同類型收窄
  • 字面量類型
  • 條件類型同映射類型

恭喜你完成第八課! 🎉

泛型係 TypeScript 嘅強大功能,掌握佢可以讓你寫出更靈活、更安全嘅程式碼。繼續練習,你會發現泛型喺實際開發中嘅巨大價值!