在 JavaScript 裡,我們常常會寫一些「通用」的函式,例如:
ts
CopyEdit
function first(arr) {
return arr[0];
}
但這個函式有兩個問題:
first([1,2,3])
回傳的型別 TS 只會推成 any
string[]
應該回傳 string
泛型(Generics)就是為了解決這種問題而存在的。
它讓我們可以把 型別變數化,讓函式、型別或類別可以針對「任意型別」運作,但同時保留型別推論與檢查能力。
泛型用尖括號 <T>
宣告,T
就是「型別參數」:
ts
CopyEdit
function first<T>(arr: T[]): T {
return arr[0];
}
const num = first([1, 2, 3]); // num: number
const str = first(["a", "b"]); // str: string
這裡的好處是:
有時候推論不準,我們可以手動給型別參數:
ts
CopyEdit
const mixed = first<number | string>([1, "b", 3]); // T 被強制為 number|string
有時候,我們希望泛型不是真的「任何型別」,而是必須符合某些條件。
例如:要取得 .length
,就必須保證傳入的東西有 length 屬性。
ts
CopyEdit
function getLength<T extends { length: number }>(value: T) {
return value.length;
}
getLength([1, 2, 3]); // OK
getLength("Hello"); // OK
getLength(123); // ❌ number 沒有 length
泛型可以有多個參數:
ts
CopyEdit
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b };
}
const user = merge({ id: 1 }, { name: "Alice" });
// user: { id: number; name: string }
這在合併物件、建立複合資料型別時非常好用。
如果呼叫時沒傳型別參數,可以給一個預設:
ts
CopyEdit
function withDefault<T = string>(value: T): T {
return value;
}
withDefault("hi"); // T = string
withDefault(123); // T = number
前面 Day 20~22 我們一直在處理 API 型別,這裡我們做一個泛型 API 函式:
ts
CopyEdit
async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
return res.json() as Promise<T>;
}
// 使用
type User = { id: string; name: string; email: string };
const user = await fetchJson<User>("/api/users/1");
console.log(user.name); // 有型別提示!
好處:
我們可以結合 Day 21 的 Pick
、Omit
:
ts
CopyEdit
async function fetchPartial<T, K extends keyof T>(
url: string,
keys: K[]
): Promise<Pick<T, K>> {
const res = await fetch(url);
const data = await res.json();
const result = {} as Pick<T, K>;
keys.forEach(k => (result[k] = data[k]));
return result;
}
// 使用
const partialUser = await fetchPartial<User, "id" | "name">("/api/users/1", ["id", "name"]);
這樣可以只取得我們要的欄位。
搭配 zod
:
ts
CopyEdit
import { z } from "zod";
function validate<T>(schema: z.ZodSchema<T>, data: unknown): T {
return schema.parse(data);
}
const userSchema = z.object({
id: z.string(),
name: z.string(),
});
const validUser = validate(userSchema, { id: "1", name: "Alice" });
錯誤 1:過度使用泛型
ts
CopyEdit
function foo<T>(value: T) { return value; }
// 如果沒必要,其實可以直接寫 function foo(value: string)
錯誤 2:忘了加泛型限制,導致型別太寬
ts
CopyEdit
function getName<T>(obj: T) {
return obj.name; // ❌ TS 不知道 T 有 name
}
→ 應該加 <T extends { name: string }>
心法: