Category: reference
TypeScript Generics — เขียน Code ที่ flexible และ type-safe
เข้าใจ TypeScript generics ตั้งแต่พื้นฐานจนถึง advanced — generic functions, constraints, conditional types, infer keyword และ utility types ที่สร้างเอง
สารบัญ
Generic คืออะไร
Generic คือ placeholder สำหรับ type ที่จะระบุทีหลัง ทำให้ function/type ทำงานได้กับหลาย type โดยยังคง type safety:
// ❌ any — ไม่มี type safety
function identity(arg: any): any {
return arg;
}
// ✅ Generic — return type ตรงกับ input type เสมอ
function identity<T>(arg: T): T {
return arg;
}
const str = identity('hello'); // str: string
const num = identity(42); // num: number
const bool = identity(true); // bool: boolean
Generic Functions
// หา element แรกที่ตรงกับ predicate
function findFirst<T>(items: T[], predicate: (item: T) => boolean): T | undefined {
return items.find(predicate);
}
const project = findFirst(projects, p => p.status === 'active');
// project: Project | undefined
// แปลง array — ชัดเจนกว่า built-in map
function transform<TInput, TOutput>(
items: TInput[],
mapper: (item: TInput) => TOutput
): TOutput[] {
return items.map(mapper);
}
const titles = transform(projects, p => p.data.title);
// titles: string[]
Constraints (extends)
จำกัด type ที่ generic รับได้:
// T ต้องมี .length property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest('abc', 'de'); // 'abc'
longest([1, 2, 3], [1]); // [1, 2, 3]
// longest(1, 2); // ❌ Error: number ไม่มี .length
// T ต้องเป็น key ของ object
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const project = { title: 'My App', status: 'active' };
getProperty(project, 'title'); // string
getProperty(project, 'status'); // string
// getProperty(project, 'url'); // ❌ Error
Generic Types และ Interfaces
// Generic interface
interface ApiResponse<T> {
data: T;
error: string | null;
status: number;
}
type ProjectResponse = ApiResponse<Project>;
type ProjectListResponse = ApiResponse<Project[]>;
// Generic class
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 stack = new Stack<string>();
stack.push('hello');
const top = stack.peek(); // string | undefined
Default Type Parameters
// ถ้าไม่ระบุ T ใช้ string เป็น default
interface Collection<T = string> {
items: T[];
count: number;
}
const strings: Collection = { items: ['a', 'b'], count: 2 }; // T = string
const numbers: Collection<number> = { items: [1, 2], count: 2 }; // T = number
Conditional Types
// ถ้า T extends string → return string[], มิฉะนั้น return number[]
type ArrayOf<T> = T extends string ? string[] : number[];
type A = ArrayOf<string>; // string[]
type B = ArrayOf<number>; // number[]
// IsArray utility
type IsArray<T> = T extends any[] ? true : false;
type C = IsArray<string[]>; // true
type D = IsArray<string>; // false
Infer Keyword
ดึง type ออกมาจาก structure:
// ดึง return type ของ function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type GetResult = ReturnType<typeof getCollection>;
// AstroCollectionEntry[]
// ดึง element type ออกจาก array
type ElementType<T> = T extends (infer E)[] ? E : never;
type StrElement = ElementType<string[]>; // string
Utility Types ที่สร้างเอง
// ทำให้ทุก property เป็น required
type RequiredDeep<T> = {
[K in keyof T]-?: T[K] extends object ? RequiredDeep<T[K]> : T[K];
};
// ดึง keys ที่มี value เป็น type ที่ระบุ
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
interface Project {
title: string;
count: number;
active: boolean;
}
type StringKeys = KeysOfType<Project, string>; // "title"
type NumberKeys = KeysOfType<Project, number>; // "count"
// Nullable — เพิ่ม null ให้ทุก property
type Nullable<T> = { [K in keyof T]: T[K] | null };
ใช้กับ Astro Content Collections
// Generic helper สำหรับ paginate array
function paginate<T>(items: T[], page: number, perPage = 10): {
items: T[];
currentPage: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
} {
const totalPages = Math.ceil(items.length / perPage);
const start = (page - 1) * perPage;
return {
items: items.slice(start, start + perPage),
currentPage: page,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1,
};
}
const { items: pageProjects, ...pagination } = paginate(allProjects, currentPage);
สรุป: เมื่อไรควรใช้ Generics
- Function ที่ทำงานเหมือนกันกับหลาย type (map, filter, find)
- Container ที่ hold type ใดก็ได้ (Stack, Queue, ApiResponse)
- เมื่อ return type ต้องสัมพันธ์กับ parameter type
- เมื่อ utility type ที่มีอยู่ (Partial, Required, Pick) ไม่เพียงพอ
อย่าใช้ generics ถ้าเขียนโดยไม่มี generic แล้ว work — ความซับซ้อนต้องแลกมาด้วยประโยชน์ที่ชัดเจน