ข้ามไปเนื้อหาหลัก

Category: reference

TypeScript Generics — เขียน Code ที่ flexible และ type-safe

เข้าใจ TypeScript generics ตั้งแต่พื้นฐานจนถึง advanced — generic functions, constraints, conditional types, infer keyword และ utility types ที่สร้างเอง

· อ่านประมาณ 4 นาที

สารบัญ

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

  1. Function ที่ทำงานเหมือนกันกับหลาย type (map, filter, find)
  2. Container ที่ hold type ใดก็ได้ (Stack, Queue, ApiResponse)
  3. เมื่อ return type ต้องสัมพันธ์กับ parameter type
  4. เมื่อ utility type ที่มีอยู่ (Partial, Required, Pick) ไม่เพียงพอ

อย่าใช้ generics ถ้าเขียนโดยไม่มี generic แล้ว work — ความซับซ้อนต้องแลกมาด้วยประโยชน์ที่ชัดเจน