ข้ามไปยังเนื้อหา

Generic Functions & Constraints

ส่วนใหญ่คุณไม่ต้องเขียน type argument เลย — inference เติมให้จาก value ที่คุณส่งเข้าไป:

function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const a = first([1, 2, 3]);
// ^? number | undefined — T inferred as number
const b = first(["x", "y"]);
// ^? string | undefined — T inferred as string

คุณ ส่ง type argument ตรง ๆ ได้ (first<number>([1, 2, 3])) แต่แทบไม่จำเป็น type parameter ไหลมาจากจุดที่เรียก

T ที่ไม่มี constraint เป็นอะไรก็ได้ ดังนั้นคุณจะสมมติว่า property อะไรไม่ได้เลย เมื่อ function ต้องการให้ T มี shape บางอย่าง ให้ constrain ด้วย extends:

// ❌ without a constraint, T might not have .length
function longest<T>(a: T, b: T) {
return a.length > b.length ? a : b; // Error: Property 'length' does not exist on type 'T'
}
// ✅ constrain T to things that have a numeric length
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest([1, 2], [1, 2, 3]); // ✅ arrays have length → returns number[]
longest("ab", "abc"); // ✅ strings have length → returns string
longest(1, 2); // ❌ number has no length

อ่าน T extends { length: number } ว่า “T ต้อง assignable ให้ { length: number }” — คือ T เป็น subtype อะไรก็ได้ที่มี length เป็น number อย่างน้อย ที่สำคัญคือ return type ยังเป็น T อยู่ ดังนั้นคุณยังได้ type ที่ เฉพาะเจาะจง (number[], string) ไม่ได้ widen ไปเป็น constraint

constraint อ้างถึง type parameter อีกตัวได้ ตัวอย่างคลาสสิกคือ get ที่ type-safe:

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Ada", age: 36 };
const name = getProp(user, "name");
// ^? string
const age = getProp(user, "age");
// ^? number
getProp(user, "email"); // ❌ "email" is not a key of user

K extends keyof T บอกว่า key ต้องเป็นหนึ่งใน key จริง ๆ ของ T และ return type T[K] คือ type ของ property ตัวนั้นเป๊ะ ๆ นี่คือกระดูกสันหลังของ utility เกี่ยวกับ object ที่ type-safe

เหมือน default argument ของ function — type parameter มี default ได้ ซึ่งจะถูกใช้เมื่อ infer ไม่ได้และไม่ได้ส่งมา:

interface Container<T = string> {
value: T;
}
const c1: Container = { value: "hi" }; // T defaults to string
const c2: Container<number> = { value: 1 }; // T explicitly number

default พบบ่อยกับ generic class และ interface ที่มีค่า fallback ที่สมเหตุสมผล

generics เป็นเครื่องมือ ไม่ใช่เป้าหมาย type parameter ที่ปรากฏ แค่ครั้งเดียว ใน signature มักเป็นความผิดพลาด — ไม่ได้เชื่อมอะไรเลย ก็เลยควรเป็น type ตรง ๆ หรือ unknown ไปเลย:

// pointless generic: T is used once, links nothing
function log<T>(x: T): void { console.log(x); }
// just write:
function log(x: unknown): void { console.log(x); }

generic จะคุ้มค่าก็ต่อเมื่อ type parameter เชื่อมตำแหน่งตั้งแต่สองที่ขึ้นไป (input ไป output, หรือ parameter ตัวหนึ่งไปอีกตัว) ถ้า type parameter ไม่ได้สร้าง ความสัมพันธ์ ก็ตัดทิ้ง

ทำไมต้อง constrain type parameter ด้วย `extends`?
ใน `function getProp<T, K extends keyof T>(obj: T, key: K): T[K]` return type `T[K]` แทนอะไร?
type parameter ที่ปรากฏแค่ครั้งเดียวใน signature มักจะ:
default type parameter อย่าง `<T = string>` ทำอะไร?