泛型(Generics)= 型別的變數。你先寫一個「模板」,真正用的時候再把型別塞進去。今天把三個常見升級招式講清楚:預設值、多參數、約束(extends / keyof)。
就像表單有預設選項。大多數情況用預設就夠,特殊情況再自己改。
// T 有預設值 string:呼叫者不填也能跑
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const a = createArray(3, "hi"); // 推論 T=string(用預設)
const b = createArray<number>(3, 99); // 手動指定 T=number
function makePair<K = string, V = number>(k: K, v: V) {
return [k, v] as [K, V];
}
makePair("id", 1); // OK → K=string, V=number(剛好等於預設)
makePair(true, 1); // OK → K=boolean(覆蓋預設)
makePair("id", "oops"); // OK → V=string(覆蓋預設)
// 反例:不要寫死 value 的型別,否則預設就沒意義
// function bad<T = string>(x: T, y: string) { ... } // y 寫死會讓 T=string顯得突兀
<T, U, V>
像三個空抽屜,執行時再放進對應物品。抽屜之間還能「互相參考」。
Map<K, V>
、Pair<L, R>
、zip<A, B>
等。function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const p1 = pair("id", 100); // [string, number]
const p2 = pair(42, { ok: true }); // [number, { ok: boolean }]
// 需求:保證 value 的型別,必須是 obj[key] 對應的型別
function setProp<
Obj extends object,
Key extends keyof Obj
>(obj: Obj, key: Key, value: Obj[Key]) {
obj[key] = value;
}
const user = { id: "u1", age: 20 };
setProp(user, "age", 99); // OK value 是 number
setProp(user, "id", "u2"); // OK value 是 string
// setProp(user, "age", "oops"); // ❌ Type 'string' is not assignable to type 'number'
Key extends keyof Obj
),TS 才能幫你「對準」。extends
像「入場門檻」,不符合條件就不讓進。
keyof T
像「T 的菜單」,只能點菜單上有的。
extends
:先檢查,再通行// 只接受有 length 屬性的東西(字串、陣列、自製物件都可)
function getLength<T extends { length: number }>(x: T) {
return x.length;
}
getLength("hello"); // OK
getLength([1, 2, 3]); // OK
// getLength(123); // ❌ Property 'length' does not exist on type 'number'
// 只允許物件(排除 number/string/boolean 等原始型別)
function keysOf<T extends object>(obj: T) {
return Object.keys(obj) as Array<keyof T>;
}
小技巧:如果你想表達「鍵是字串、值隨意」,可用 Record<string, unknown>。
keyof
:鍵名也能變型別type User = { id: string; age: number };
type UserKeys = keyof User; // "id" | "age"
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
for (const k of keys) out[k] = obj[k];
return out;
}
const u = { id: "u1", age: 20, name: "Bob" };
pick(u, ["id", "age"]); // OK → { id: string; age: number }
// pick(u, ["email"]); // ❌ Argument of type '("email")[]' is not assignable to parameter of type '("id" | "age" | "name")[]'
Pick<T, K>
能這麼準?K extends keyof T
:K 只能是 T 的鍵。Pick<T, K>
:把 T 裡屬性名在 K 這一組的挑出來,形成新的物件型別。我們用一段程式,拆解 TS 腦中發生的事:
function getProp<T, K extends keyof T>(obj: T, key: K) {
return obj[key]; // 型別是 T[K]
}
const book = { title: "TS", pages: 300 as const };
const t = getProp(book, "title");
// 推論流程:
// 1) T = { title: string; pages: 300 }(字面量推論)
// 2) K = "title"(因為第二個參數)
// 3) 回傳型別 = T[K] = T["title"] = string
const p = getProp(book, "pages");
// 回傳型別 = T["pages"] = 300(字面量型別,不是 number!)
小觀念:as const 讓值變成最窄(literal)型別,回傳就更精準。
需求:
fetchData<T>
預設 T = unknown
(比 {}
更合理,因為「未知」比「空物件」更精準)GET
或 POST
select<T, K extends keyof T>
幫你「挑欄位」type HttpMethod = "GET" | "POST";
type ApiResponse<T> = {
data: T;
status: number;
message: string;
};
async function fetchData<T = unknown>(
url: string,
method: HttpMethod = "GET"
): Promise<ApiResponse<T>> {
const res = await fetch(url, { method });
return res.json();
}
// 挑欄位的小工具(可串在一起用)
function select<T, K extends keyof T>(data: T, keys: K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
for (const k of keys) out[k] = data[k];
return out;
}
// 使用:
type User = { id: string; name: string; email: string };
fetchData<User>("/api/user").then((resp) => {
const slim = select(resp.data, ["id", "name"]); // { id: string; name: string }
});
never
function first<T>(arr: T[]): T {
return arr[0];
}
const x = first([]); // 推論 T=never → 回傳 never(很難用)
const y = first<string>([]); // ✅ 手動指定 T
// 過度抽象,使用者不知道該怎麼餵 T
function wrap<T>(x: T) { return [x]; }
// 改善:從參數推論 + 文件化
const w1 = wrap("hi"); // T=string,自然
extends
導致屬性取值報錯function values<T>(obj: T) {
return Object.keys(obj).map(k => obj[k]); // ❌ obj[k] 報錯,因為 T 可能不是 object
}
// 修正:
function values<T extends object>(obj: T) {
return Object.keys(obj).map(k => (obj as any)[k]); // 或搭配 Record 來更安全
}
// 或者更嚴謹:
function values2<T extends Record<string, unknown>>(obj: T) {
return Object.keys(obj).map(k => obj[k]);
}
unknown
通常比 {}
更貼切)?extends
把「入場門檻」講清楚?keyof
?