TypeScript(TS)是一種靜態型別的超集合語言,旨在增強 JavaScript 的可維護性和可讀性。在 TS 中,泛型(Generics) 是一個強大而靈活的特性,允許我們在編寫函數、類別或接口時,不必預先定義型別,而是將型別作為參數來使用,進而實現更加靈活和可重用的代碼結構。


什麼是泛型?

泛型可以理解為「型別變數」,它允許開發者在編寫代碼時不指定具體型別,而是在使用時再決定具體型別。這使得代碼能適應更多場景,而不失去型別檢查的優勢。

泛型的基本語法

泛型使用尖括號 <T> 來表示,其中 T 是型別參數的名稱,可以是任何有效的標識符。

1
2
3
function identity<T>(value: T): T {
return value;
}

使用泛型函數

在上述範例中,我們定義了一個泛型函數 identity,它接收任意型別的值並返回相同型別的值。使用時可以顯式地指定型別,或讓編譯器自動推斷。

1
2
3
4
5
// 顯式指定型別
const result1 = identity<string>("Hello, TypeScript"); // result1: string

// 型別推斷
const result2 = identity(123); // result2: number

泛型的應用場景

1. 泛型與陣列

我們可以用泛型來確保陣列中所有元素具有相同型別。

1
2
3
4
5
6
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}

const firstNumber = getFirstElement([1, 2, 3]); // firstNumber: number
const firstString = getFirstElement(["a", "b", "c"]); // firstString: string

2. 泛型與類別

泛型可以用於類別來定義靈活的資料結構。例如,一個通用的堆疊類別。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Stack<T> {
private items: T[] = [];

push(item: T): void {
this.items.push(item);
}

pop(): T | undefined {
return this.items.pop();
}
}

const numberStack = new Stack<number>();
numberStack.push(10);
console.log(numberStack.pop()); // 10

3. 泛型與接口

泛型可以幫助接口適應多種型別場景。

1
2
3
4
5
6
7
8
9
interface KeyValuePair<K, V> {
key: K;
value: V;
}

const pair: KeyValuePair<string, number> = {
key: "age",
value: 30,
};

4. 泛型約束

有時候我們希望泛型不只是「任意型別」,而是需要滿足某些條件。這可以通過 extends 關鍵字來實現。

1
2
3
4
5
6
function logProperty<T extends { name: string }>(obj: T): void {
console.log(obj.name);
}

logProperty({ name: "Alice", age: 25 }); // Alice
// logProperty(123); // 錯誤,123 不符合約束

泛型 vs any

很多開發者可能會想:「為什麼要使用泛型?直接使用 any 不是更簡單嗎?」

泛型與 any 都能處理多種型別,但它們有本質上的區別:

any 的特性

  1. 靈活但無約束any 允許任何型別的值,但在編譯階段不進行型別檢查。
  2. 可能導致型別安全問題any 使得編譯器無法提供錯誤提示,容易引發運行時錯誤。

範例:

1
2
3
4
5
6
function processValue(value: any): any {
return value;
}

const result = processValue(123); // result: any
result.toUpperCase(); // 運行時錯誤,因為數字沒有 toUpperCase 方法

泛型的特性

  1. 靈活且安全:泛型在保證靈活性的同時,仍然保留了型別檢查。
  2. 提升代碼可讀性:泛型讓函數或類別的型別關係更加明確。

範例:

1
2
3
4
5
6
function processValue<T>(value: T): T {
return value;
}

const result = processValue(123); // result: number
// result.toUpperCase(); // 編譯錯誤,避免了運行時錯誤

比較表

特性 any 泛型 (T)
靈活性
型別安全性
編譯階段檢查
可讀性 可能模糊 清晰,與業務邏輯緊密相關
適用場景 型別未知的臨時解決方案 可重用的泛型結構

何時使用泛型而不是 any

  1. 當你需要在函數內部使用參數的特定方法或屬性時
  2. 當你需要確保函數的返回值類型與輸入參數類型相關時
  3. 當你需要在編譯時捕獲可能的型別錯誤時
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 好的實踐:使用泛型
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}

// 不好的實踐:使用 any
function firstElementAny(arr: any[]): any {
return arr[0];
}

// 使用泛型的好處
const numbers = [1, 2, 3];
const firstNumber = firstElement(numbers); // TypeScript 知道這是 number 類型
const strings = ["a", "b", "c"];
const firstString = firstElement(strings); // TypeScript 知道這是 string 類型

// 使用 any 的問題
const firstAny = firstElementAny(numbers); // TypeScript 不知道具體類型
const len: number = firstAny.length; // 不會報錯,但可能在運行時出錯

實際應用場景

1. API 響應處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
return response.json();
}

// 使用例子
interface User {
id: number;
name: string;
}

const user = await fetchData<User>('/api/user/1');

2. 狀態管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class State<T> {
private value: T;

constructor(initialValue: T) {
this.value = initialValue;
}

getValue(): T {
return this.value;
}

setValue(newValue: T): void {
this.value = newValue;
}
}

const numberState = new State<number>(0);
const stringState = new State<string>("");

最佳實踐

  1. 明確的類型參數名稱:使用有意義的泛型參數名稱,例如在處理集合時使用 T,在處理鍵值對時使用 K 和 V。

  2. 適度使用泛型約束:使用 extends 關鍵字來限制泛型類型,確保類型安全。

  3. 避免過度使用:只在真正需要靈活性的地方使用泛型,過度使用會增加程式碼的複雜度。

結論

泛型是一個強大的特性,它能幫助我們寫出更靈活、更可重用的程式碼,同時保持類型安全。

記住,泛型的主要目的是幫助我們寫出更通用的程式碼,同時保持類型安全。在實際開發中,合理使用泛型可以大大提高程式碼的可維護性和重用性。