TypeScript 泛型約束與預設值
泛型非常靈活,但有時候我們需要限制它的範圍。本篇將介紹如何約束和控制泛型。
一、 為什麼需要約束?
1.1 問題場景
typescript
function getLength<T>(value: T): number {
return value.length; // 錯誤!T 不一定有 length
}1.2 使用 extends 約束
typescript
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(value: T): number {
return value.length; // 現在可以了!
}
getLength("hello"); // 5
getLength([1, 2, 3]); // 3
getLength({ length: 10 }); // 10
// getLength(123) // 錯誤!number 沒有 length二、 泛型約束
2.1 基本語法
typescript
// T 必須符合某個類型
function fn<T extends SomeType>(value: T): T {
return value;
}2.2 約束為物件
typescript
function printInfo<T extends { name: string }>(obj: T) {
console.log(obj.name);
}
printInfo({ name: "John", age: 30 }); // OK
printInfo({ name: "Jane" }); // OK
// printInfo({ age: 30 }) // 錯誤!缺少 name2.3 約束為函式
typescript
function call<T extends (...args: any[]) => any>(
fn: T,
...args: Parameters<T>
): ReturnType<T> {
return fn(...args);
}
const result = call(Math.max, 1, 2, 3); // 3三、 keyof 運算子
3.1 基本用法
keyof 取得物件類型的所有鍵:
typescript
interface User {
id: number;
name: string;
email: string;
}
type UserKeys = keyof User; // 'id' | 'name' | 'email'3.2 搭配泛型
typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "John", email: "john@example.com" };
const id = getProperty(user, "id"); // number
const name = getProperty(user, "name"); // string
// getProperty(user, 'age') // 錯誤!'age' 不存在3.3 實作 pick 函式
typescript
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach((key) => {
result[key] = obj[key];
});
return result;
}
const user = { id: 1, name: "John", email: "john@example.com" };
const partial = pick(user, ["name", "email"]);
// { name: string, email: string }四、 多重約束
4.1 交叉約束
typescript
interface HasId {
id: number;
}
interface HasName {
name: string;
}
// T 必須同時有 id 和 name
function printEntity<T extends HasId & HasName>(entity: T) {
console.log(`${entity.id}: ${entity.name}`);
}
printEntity({ id: 1, name: "John", email: "john@example.com" });4.2 類別約束
typescript
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
function createAnimal<T extends Animal>(
AnimalClass: new (name: string) => T,
name: string
): T {
return new AnimalClass(name);
}五、 泛型預設值
5.1 基本語法
typescript
interface ApiResponse<T = unknown> {
success: boolean;
data: T;
}
// 使用預設值
const response1: ApiResponse = { success: true, data: null };
// data: unknown
// 指定類型
const response2: ApiResponse<User> = {
success: true,
data: { id: 1, name: "John" },
};
// data: User5.2 函式泛型預設值
typescript
function createContainer<T = string>(value: T): { value: T } {
return { value };
}
const container1 = createContainer("hello"); // { value: string }
const container2 = createContainer<number>(42); // { value: number }5.3 多參數預設值
typescript
interface State<TData = null, TError = Error> {
data: TData;
error: TError | null;
loading: boolean;
}
// 使用全部預設
const state1: State = { data: null, error: null, loading: false };
// 只指定第一個
const state2: State<User> = { data: null, error: null, loading: false };
// 指定全部
const state3: State<User, string> = { data: null, error: null, loading: false };六、 實用模式
6.1 可選鍵
typescript
function getOptionalProperty<T, K extends keyof T>(
obj: T,
key: K
): T[K] | undefined {
return obj[key];
}6.2 部分更新
typescript
function updateObject<T extends object>(target: T, updates: Partial<T>): T {
return { ...target, ...updates };
}
const user = { id: 1, name: "John", age: 30 };
const updated = updateObject(user, { name: "Jane" });
// { id: 1, name: 'Jane', age: 30 }6.3 類型安全的事件發射器
typescript
type EventMap = {
click: { x: number; y: number };
keydown: { key: string };
};
class EventEmitter<T extends Record<string, object>> {
private listeners: Partial<{ [K in keyof T]: ((data: T[K]) => void)[] }> = {};
on<K extends keyof T>(event: K, listener: (data: T[K]) => void) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof T>(event: K, data: T[K]) {
this.listeners[event]?.forEach((listener) => listener(data));
}
}
const emitter = new EventEmitter<EventMap>();
emitter.on("click", (data) => console.log(data.x, data.y));
emitter.emit("click", { x: 10, y: 20 });總結
| 語法 | 說明 | 範例 |
|---|---|---|
extends | 類型約束 | T extends HasLength |
keyof | 取得鍵 | keyof User |
T[K] | 索引存取 | obj[key] |
= Type | 預設值 | T = string |
> **設計原則**:
- 約束越精確越好
- 善用
keyof保證鍵安全 - 預設值讓 API 更彈性
進階挑戰
- 實作一個類型安全的
omit函式 - 實作一個
merge函式,合併兩個物件並保持類型
typescript
// 練習 2 提示
function merge<T extends object, U extends object>(a: T, b: U): T & U {
// 實作...
}- 為
fetch封裝一個類型安全的包裝函式