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

Generic Classes & Interfaces

interface หรือ type alias รับ type parameter ได้เหมือน function เป๊ะ ตัวคลาสสิกคือ container ที่ type แล้ว:

interface Box<T> {
value: T;
}
const numBox: Box<number> = { value: 42 };
const strBox: Box<string> = { value: "hi" };
numBox.value.toFixed(2); // ✅ value is number

type alias ทำงานเหมือนกัน และมักดีกว่าสำหรับ union และ function:

type Pair<A, B> = { first: A; second: B };
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
const r: Result<number> = { ok: true, value: 10 };

Result<T> คือ generic discriminated union — หนึ่งใน pattern ที่มีประโยชน์ที่สุดใน TypeScript และเป็นสิ่งที่เราจะกลับมาพูดในบท error-handling

เมื่อ class เป็น generic type parameter ของตัวเองจะถูกเลือก ตอนที่คุณ construct instance และทุก method ของ instance นั้นใช้ร่วมกัน:

class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
const numbers = new Stack<number>();
numbers.push(1);
numbers.push("two"); // ❌ Argument of type 'string' is not assignable to 'number'
const top = numbers.pop();
// ^? number | undefined

พอเขียน new Stack<number>() T ก็เป็น number ตลอดอายุของ object นั้น หลายครั้งคุณไม่ต้องเขียนด้วยซ้ำ — new Stack() แล้ว push(1) ครั้งแรก infer ไม่ได้ แต่หลาย design ให้ inference เติม T จาก argument ของ constructor

type parameter ที่ประกาศบน method (ไม่ใช่บน class) จะถูกเลือกใหม่ทุกครั้งที่เรียก อิสระจาก parameter ของ class:

class Collection<T> {
constructor(private items: T[]) {}
// T is the class parameter (fixed per instance)
first(): T | undefined {
return this.items[0];
}
// U is a method parameter (fresh per call)
map<U>(fn: (item: T) => U): U[] {
return this.items.map(fn);
}
}
const c = new Collection([1, 2, 3]); // T = number
const lengths = c.map((n) => n.toString().length);
// ^? number[] — U inferred as number, per this call
const labels = c.map((n) => `item-${n}`);
// ^? string[] — U inferred as string, a different call

T ถูกตัดสินครั้งเดียว (ตอน construct) ส่วน U ถูกตัดสินทุกครั้งที่เรียก map การรู้ว่า parameter ตัวไหนเป็นของ class และตัวไหนเป็นของ method คือกุญแจในการอ่าน signature ของ generic class

class จะ implement generic interface ได้โดย fix parameter หรือคงความ generic ไว้เอง:

interface Repository<T> {
getById(id: string): T | undefined;
save(entity: T): void;
}
// fix the parameter to a concrete type
class UserRepository implements Repository<User> {
getById(id: string): User | undefined { /* ... */ return undefined; }
save(entity: User): void { /* ... */ }
}
สำหรับ `class Stack<T>` type parameter `T` ถูกตัดสินเมื่อไร?
ใน class `Collection<T>` ที่มี method `map<U>(...)` `T` กับ `U` ต่างกันอย่างไร?
`type Result<T> = { ok: true; value: T } | { ok: false; error: string }` เป็นตัวอย่างของอะไร?
class จะทำตาม `Repository<T>` สำหรับ entity หนึ่ง ๆ ได้อย่างไร?