TypeScript Generics คืออะไร? สอน Generic Types แบบเจาะลึก 2026
ในโลกของการพัฒนาแอปพลิเคชันสมัยใหม่ที่ความซับซ้อนของโค้ดเพิ่มสูงขึ้น การรักษา Type Safety ในขณะที่ต้องการความยืดหยุ่นสูงสุดเป็นความท้าทายสำคัญ TypeScript เกิดขึ้นมาเพื่อแก้ไขจุดนี้ และหนึ่งในอาวุธลับที่ทรงพลังที่สุดของมันก็คือ Generics หากคุณเป็นนักพัฒนาที่เคยเขียนฟังก์ชันหรือคลาสที่ทำงานกับข้อมูลหลายประเภท แล้วรู้สึกว่าตัวเองกำลังคัดลอกโค้ดหรือต้องใช้ `any` ซึ่งทำให้เสียประโยชน์ของ Type Checking ไป บทความเจาะลึกนี้สำหรับคุณโดยเฉพาะ เราจะพาไปสำรวจโลกของ TypeScript Generics ตั้งแต่พื้นฐานจนถึงเทคนิคขั้นสูง พร้อมตัวอย่างโค้ดที่ใช้งานได้จริงและกรณีศึกษาในปี 2026
Generics คืออะไร? ทำไมถึงสำคัญใน TypeScript?
TypeScript Generics (หรือ Generic Types) คือเครื่องมือที่ช่วยให้เราสร้าง component ที่สามารถทำงานกับข้อมูลได้หลายประเภท (types) โดยยังคงรักษาข้อมูลประเภทนั้นๆ ไว้ (preserve type information) แทนที่จะใช้ประเภทใดประเภทหนึ่งตายตัวหรือใช้ `any` ซึ่งเป็นการยอมแพ้ต่อระบบประเภทข้อมูล
คิดง่ายๆ ว่า Generics เป็นเหมือน "ตัวแปรสำหรับประเภทข้อมูล" (type variable) ที่เราส่งผ่านเข้าไปในฟังก์ชัน คลาส อินเตอร์เฟซ หรือ type alias ในเวลาที่เรียกใช้ ทำให้ component นั้นๆ สามารถปรับตัวเข้ากับประเภทข้อมูลที่เราต้องการได้ทันที โดยที่ TypeScript Compiler จะช่วยตรวจสอบความถูกต้องให้ตลอดทาง
ความสำคัญหลักอยู่ที่:
- Reusability: เขียนโค้ดครั้งเดียว แต่ใช้กับประเภทข้อมูลได้หลากหลาย
- Type Safety: หลีกเลี่ยงการใช้ `any` ซึ่งทำให้เสียการตรวจสอบประเภทและคำแนะนำจาก Editor (IntelliSense)
- Constraint & Flexibility: กำหนดขอบเขตของประเภทที่อนุญาตได้ (Constraint) ในขณะที่ยังยืดหยุ่นอยู่
- Maintainability: เมื่อต้องการเปลี่ยน logic, การแก้ไขทำที่เดียวและส่งผลถึงทุกที่ที่ใช้ Generics นั้น
Syntax พื้นฐาน: เริ่มต้นกับ Generic Functions
มาเริ่มจากตัวอย่างคลาสสิก: ฟังก์ชันที่คืนค่าอะไรก็ตามที่รับเข้าไป (identity function) หากไม่ใช้ Generics เราอาจต้องเขียนหลายฟังก์ชันหรือใช้ `any`
// ปัญหาเมื่อไม่ใช้ Generics
function identity(arg: any): any {
return arg;
}
const output = identity("hello"); // Type ของ output คือ 'any'! เราเสียข้อมูล type ไป
// วิธีแก้ด้วย Generics
function identity<T>(arg: T): T {
return arg;
}
// เรียกใช้งาน
const stringOutput = identity<string>("Hello Generics"); // Type: string
const numberOutput = identity<number>(42); // Type: number
const inferredOutput = identity("Type Inference works!"); // TypeScript Infer Type เป็น string ให้อัตโนมัติ
ในตัวอย่างด้านบน `
Generic Interfaces และ Type Aliases
เราสามารถใช้ Generics กับโครงสร้างข้อมูลได้อย่างมีประสิทธิภาพ
// Generic Interface
interface ApiResponse<T> {
success: boolean;
statusCode: number;
data: T; // ประเภทของ data จะถูกกำหนดตอนใช้งาน interface
timestamp: Date;
}
// การใช้งาน
const userResponse: ApiResponse<{ id: number; name: string }> = {
success: true,
statusCode: 200,
data: { id: 1, name: "John Doe" },
timestamp: new Date()
};
const productResponse: ApiResponse<string[]> = {
success: true,
statusCode: 200,
data: ["Laptop", "Mouse", "Keyboard"],
timestamp: new Date()
};
// Generic Type Alias
type Pair<T, U> = {
first: T;
second: U;
};
const keyValue: Pair<string, number> = { first: "age", second: 30 };
const coordinates: Pair<number, number> = { first: 10.5, second: -20.3 };
Generic Constraints: กำหนดขอบเขตให้กับ Type Parameters
บางครั้งเราไม่ต้องการให้ `T` เป็นอะไรก็ได้ แต่ต้องการให้มีคุณสมบัติบางอย่าง เช่น มี property `length` หรือเป็น object ที่มีโครงสร้างขั้นต่ำบางอย่าง นี่คือที่มาของ `extends` keyword
// Constraint พื้นฐาน: ต้องการให้มี property length
function logLength<T extends { length: number }>(arg: T): T {
console.log(`Length: ${arg.length}`);
return arg;
}
logLength("hello"); // OK, string มี .length
logLength([1, 2, 3]); // OK, array มี .length
logLength({ length: 10, name: "custom" }); // OK
// logLength(42); // Error! number ไม่มี property .length
// Constraint ร่วมกับ keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Alice", age: 28 };
const name = getProperty(person, "name"); // Type: string
const age = getProperty(person, "age"); // Type: number
// const unknown = getProperty(person, "salary"); // Error! "salary" ไม่ใช่ key ของ obj
Generic Classes สำหรับการสร้างโครงสร้างที่นำกลับมาใช้ใหม่ได้
คลาส Generics มีประโยชน์มากสำหรับโครงสร้างข้อมูลเช่น คลังเก็บ (Repository), คิว (Queue), สแต็ก (Stack) หรือตัวจัดการสถานะ (State Manager)
class GenericStack<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];
}
size(): number {
return this.items.length;
}
}
// ใช้งาน Stack สำหรับประเภทต่างๆ
const numberStack = new GenericStack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2, Type: number | undefined
const stringStack = new GenericStack<string>();
stringStack.push("first");
stringStack.push("second");
// ตัวอย่างที่ซับซ้อนขึ้น: Generic Repository
interface Identifiable {
id: number | string;
}
class GenericRepository<T extends Identifiable> {
private entities: Map<T['id'], T> = new Map();
add(entity: T): void {
this.entities.set(entity.id, entity);
}
getById(id: T['id']): T | undefined {
return this.entities.get(id);
}
getAll(): T[] {
return Array.from(this.entities.values());
}
}
interface User extends Identifiable {
id: number;
name: string;
email: string;
}
const userRepo = new GenericRepository<User>();
userRepo.add({ id: 1, name: "Bob", email: "bob@example.com" });
const foundUser = userRepo.getById(1); // Type: User | undefined
เทคนิคขั้นสูง: Conditional Types, Mapped Types และ Utility Types
ใน TypeScript รุ่นใหม่ๆ (รวมถึงปี 2026) มีฟีเจอร์ขั้นสูงที่ทำงานร่วมกับ Generics ได้อย่างน่าทึ่ง
Conditional Types
สร้างประเภทที่เปลี่ยนแปลงได้ตามเงื่อนไข (Ternary Operator สำหรับ Types)
type IsString<T> = T extends string ? "Yes" : "No";
type A = IsString<string>; // "Yes"
type B = IsString<number>; // "No"
// ตัวอย่างใช้งานจริง: Extract และ Exclude
type ExtractType<T, U> = T extends U ? T : never;
type ExcludeType<T, U> = T extends U ? never : T;
type T0 = ExtractType<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = ExcludeType<"a" | "b" | "c", "a" | "f">; // "b" | "c"
// ใช้กับฟังก์ชัน
type ReturnTypeIfString<T> = T extends (...args: any[]) => string ? T : never;
function onlyStringFunctions<T extends (...args: any[]) => any>(fn: ReturnTypeIfString<T>): void {
// ฟังก์ชันนี้รับเฉพาะฟังก์ชันที่คืนค่า string
}
Mapped Types และ key remapping
// สร้าง type ที่ทำให้ทุก property เป็น optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// สร้าง type ที่ทำให้ทุก property เป็น readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// ตัวอย่างที่ซับซ้อน: สร้าง type จาก enum หรือ union type
type EventTypes = "click" | "scroll" | "keypress";
type HandlerMap = {
[E in EventTypes]: (event: Event) => void;
};
// ผลลัพธ์: { click: (event: Event) => void; scroll: (event: Event) => void; keypress: (event: Event) => void; }
// Key Remapping (as clause)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// ผลลัพธ์: { getName: () => string; getAge: () => number; }
Generic Constraints ขั้นสูงกับ Default Types และ Infer
เราสามารถกำหนดค่าเริ่มต้นให้ Type Parameters และใช้ `infer` keyword เพื่อดึง subtype ออกมาได้
// Default Type Parameters
interface PaginatedResult<T = any> { // ค่า default คือ any
data: T[];
page: number;
totalPages: number;
}
const result1: PaginatedResult = { data: [1,2,3], page: 1, totalPages: 5 }; // T เป็น any
const result2: PaginatedResult<string> = { data: ["a","b"], page: 1, totalPages: 2 }; // T เป็น string
// ใช้หลาย parameters พร้อม default
function createPair<T = string, U = number>(first: T, second: U): [T, U] {
return [first, second];
}
// Infer Keyword (มักใช้กับ Conditional Types)
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type ElementType = UnpackArray<number[]>; // number
type NotArray = UnpackArray<string>; // string
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type FnReturn = ReturnType<() => boolean>; // boolean
กรณีศึกษาในปี 2026: Generics กับ Modern Full-Stack Development
ในสภาพแวดล้อมการพัฒนาปี 2026 ที่มี Microservices, Serverless Functions และการจัดการสถานะที่ซับซ้อน Generics ยังคงเป็นหัวใจสำคัญ
1. Type-Safe API Client
// กำหนดประเภทสำหรับ Endpoint ต่างๆ
type EndpointConfig = {
"/api/users": { params: {}; response: User[] };
"/api/users/:id": { params: { id: string }; response: User };
"/api/products": { params: { category?: string }; response: Product[] };
};
// Generic API Client
async function fetchApi<Path extends keyof EndpointConfig>(
path: Path,
params: EndpointConfig[Path]['params']
): Promise<EndpointConfig[Path]['response']> {
const url = constructUrl(path, params);
const response = await fetch(url);
return await response.json();
}
// ใช้งาน: ได้รับ Type Safety เต็มรูปแบบ
const users = await fetchApi("/api/users", {}); // Type: Promise<User[]>
const user = await fetchApi("/api/users/:id", { id: "123" }); // Type: Promise<User>
// const error = await fetchApi("/api/users", { id: "123" }); // Error! params ไม่ตรงกัน
2. Generic State Management Hook (React-like)
import { useState, useEffect } from 'react';
function useFetch<T>(url: string, initialValue: T): {
data: T;
loading: boolean;
error: Error | null;
} {
const [data, setData] = useState<T>(initialValue);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(json => setData(json))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// ใช้งานกับประเภทใดก็ได้
const { data: users, loading } = useFetch<User[]>("/api/users", []);
const { data: config } = useFetch<Config>("/api/config", { theme: "dark" });
ตารางเปรียบเทียบ: Generics vs Any vs Overloads
| เกณฑ์ | Generics | Any | Function Overloads |
|---|---|---|---|
| Type Safety | สูงสุด (รักษา type information) | ต่ำสุด (ไม่มี type checking) | สูง (แต่ต้องประกาศทุก signature) |
| ความยืดหยุ่น | สูงมาก (รองรับ type อนาคตโดยไม่ต้องแก้โค้ด) | สูงเกินไป (รับอะไรก็ได้) | จำกัด (ต้องรู้ type ล่วงหน้าทั้งหมด) |
| การนำกลับมาใช้ใหม่ | ดีเยี่ยม (เขียนครั้งเดียวใช้ทุก type) | ดี (แต่เสี่ยงต่อ error) | แย่ (ต้องเพิ่ม signature ทุกครั้งที่มี type ใหม่) |
| IntelliSense / Autocomplete | ทำงานเต็มที่ | ไม่มี | ทำงานได้ดี |
| ความซับซ้อนของโค้ด | ปานกลาง (ต้องเข้าใจ concept) | ต่ำสุด | สูง (signature เยอะ, implementation ซับซ้อน) |
| เหมาะกับ | Utility functions, Data structures, Reusable components | Prototyping, เรียก external JS libraries | ฟังก์ชันที่มีรูปแบบ parameter/return type ชัดเจนไม่กี่แบบ |
