如果說 TypeScript 是一門對類型進行編程的語言,那么泛型就是這門語言里的(函數)參數。本章,我將會從多角度講解?TypeScript 中無處不在的泛型,以及它在類型別名、對象類型、函數與 Class 中的使用方式。
一、泛型的核心概念
1.基本定義
泛型(Generics)是 TypeScript 中允許在定義函數、接口或類時不預先指定具體類型,而是在使用時動態指定類型的機制。其核心目標是實現代碼的可重用性與類型安全。
- 示例:
function identity<T>(arg: T): T { return arg; }
此處?
<T>
?為類型參數,T
?在調用時被具體類型替換,如?identity<string>("hello")
。 -
不過上述例子中直接??
identity("hello") 也是可以的,
省略不寫類型參數的值,讓 TypeScript 自己推斷。但有些復雜的使用場景,TypeScript 可能推斷不出類型參數的值,這時就必須顯式給出了。function comb<T>(arr1:T[], arr2:T[]):T[] {return arr1.concat(arr2); }comb([1, 2], ['a', 'b']) // 報錯comb<number|string>([1, 2], ['a', 'b']) // 正確
上面示例中,兩個參數
arr1
、arr2
和返回值都是同一個類型。如果不給出類型參數的值,調用會報錯。如果類型參數是一個聯合類型,就不會報錯。
2.?泛型 vs?any
any
?的缺陷:放棄類型檢查,失去 TypeScript 的類型安全優勢。- 泛型的優勢:保留類型信息,編譯器可進行靜態檢查,如類型推斷與錯誤提示 。
二、泛型的主要應用場景
泛型主要用在四個場合:函數、接口、類和別名。
1. 函數的泛型寫法
通過泛型定義可處理多種類型的函數,避免重復代碼:
function reverse<T>(items: T[]): T[] {return items.reverse();
}
const numbers = reverse([1, 2, 3]); // 推斷為 number[]
const strings = reverse(["a", "b"]); // 推斷為 string[]
此例中,T
自動匹配輸入數組類型,返回值類型與輸入一致。
2.?接口的泛型寫法
定義靈活的類型契約,適用于容器類場景:
interface KeyValuePair<K, V> {key: K;value: V;
}
const pair: KeyValuePair<number, string> = { key: 1, value: "one" };
接口通過類型參數 K
和 V
支持多種鍵值組合。
3. 類的泛型寫法
創建可復用的數據結構(如集合、棧、隊列):
class Stack<T> {private items: T[] = [];push(item: T) { this.items.push(item); }pop(): T | undefined { return this.items.pop(); }
}
const numberStack = new Stack<number>();
numberStack.push(42); // 僅允許 number 類型
此類實現保證了棧內元素的類型一致性。
4. 類型別名的泛型寫法
type 命令定義的類型別名,也可以使用泛型。
type Nullable<T> = T | undefined | null;
上面示例中,Nullable<T>
是一個泛型,只要傳入一個類型,就可以得到這個類型與undefined
和null
的一個聯合類型。
三、高級泛型技巧
1. 泛型約束
通過 extends
限制類型參數的范圍:
interface HasLength { length: number; }
function logLength<T extends HasLength>(arg: T): void {console.log(arg.length);
}
logLength("hello"); // 合法(length=5)
logLength(42); // 錯誤:缺少 length 屬性
此約束確保類型參數必須包含指定屬性。
2. 多類型參數與默認值
多類型參數:
function swap<T, U>(tuple: [T, U]): [U, T] {return [tuple[1], tuple[0]];
}
swap([7, "seven"]); // 返回 ["seven", 7]
但是類型參數越少越好,下面我會講到
默認類型:
class Generic<T = string> {list:T[] = []add(t:T) {this.list.push(t)}
}const g = new Generic();
g.add(4) // 報錯
g.add('hello') // 正確-------------------------------------------const g = new Generic<number>();
g.add(4) // 正確
g.add('hello') // 報錯
上面示例中,類Generic
有一個類型參數T
,默認值為string
。這意味著,屬性list
默認是一個字符串數組,方法add()
的默認參數是一個字符串。所以,向add()
方法傳入一個數值會報錯,傳入字符串就不會。反之,傳入字符串會報錯。
3.?索引類型與?keyof
確保對象屬性訪問的安全性:
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {return obj[key];
}
const person = { name: "Alice", age: 30 };
getValue(person, "name"); // 合法
getValue(person, "gender"); // 錯誤:屬性不存在。
4.?條件類型與映射類型
條件類型:根據條件選擇類型:
type Check<T> = T extends string ? "string" : "not string";
type A = Check<"hello">; // "string"
映射類型:基于已有類型生成新類型:
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type ReadonlyPerson = Readonly<Person>; // 所有屬性變為只讀。
四、泛型的正確使用場景與注意點
1.正確使用場景
- 當需要在多個位置(參數、返回值、成員變量)之間建立類型約束時。
- 避免重復編寫相似邏輯的類型特定代碼(如不同數據類型的隊列實現)。
2.注意點
1、盡量少用泛型。
泛型雖然靈活,但是會加大代碼的復雜性,使其變得難讀難寫。一般來說,只要使用了泛型,類型聲明通常都不太易讀,容易寫得很復雜。因此,可以不用泛型就不要用。
2、類型參數越少越好。
多一個類型參數,多一道替換步驟,加大復雜性。因此,類型參數越少越好。
function filter<T,Fn extends (arg:T) => boolean
>(arr:T[],func:Fn
): T[] {return arr.filter(func);
}
上面示例有兩個類型參數,但是第二個類型參數?Fn?
是不必要的,完全可以直接寫在函數參數的類型聲明里面。
function filter<T>(arr:T[],func:(arg:T) => boolean
): T[] {return arr.filter(func);
}
上面示例中,類型參數簡化成了一個,效果與前一個示例是一樣的。
3、類型參數需要出現兩次。
如果類型參數在定義后只出現一次,那么很可能是不必要的。
function greet<Str extends string>(s:Str
) {console.log('Hello, ' + s);
}
上面示例中,類型參數Str
只在函數聲明中出現一次(除了它的定義部分),這往往表明這個類型參數是不必要。
function greet(s:string) {console.log('Hello, ' + s);
}
上面示例把前面的類型參數省略了,效果與前一個示例是一樣的。
也就是說,只有當類型參數用到兩次或兩次以上,才是泛型的適用場合。
4、泛型可以嵌套。
類型參數可以是另一個泛型。
type OrNull<Type> = Type|null;
type OneOrMany<Type> = Type|Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
上面示例中,最后一行的泛型OrNull
的類型參數,就是另一個泛型OneOrMany
。
五、實戰應用案例
1.?React 組件泛型
定義可接收多種 props 類型的組件:
interface ListProps<T> {items: T[];renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {return <div>{items.map(renderItem)}</div>;
}
// 使用
<List<number> items={[1, 2]} renderItem={(n) => <div>{n}</div>} />。
2.?API 請求封裝
利用泛型約束返回數據類型:
async function fetchData<T>(url: string): Promise<T> {const response = await fetch(url);return response.json() as T;
}
interface User { id: number; name: string; }
const users = await fetchData<User[]>("/api/users");。