Home > Blog > tech

TypeScript Generics คืออะไร? สอน Generic Types แบบเจาะลึก 2026

typescript generics deep dive
TypeScript Generics คืออะไร? สอน Generic Types แบบเจาะลึก 2026
2026-04-16 | tech | 1336 words


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 จะช่วยตรวจสอบความถูกต้องให้ตลอดทาง

ความสำคัญหลักอยู่ที่:

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 ให้อัตโนมัติ

ในตัวอย่างด้านบน `` คือการประกาศ Type Parameter ชื่อ `T` (ตามธรรมเนียม ใช้ตัวพิมพ์ใหญ่เดียว เช่น T, U, V, K, E) ฟังก์ชัน `identity(arg: T): T` หมายความว่า: "รับพารามิเตอร์ `arg` ที่มีประเภท `T` อะไรก็ได้ แล้วส่งคืนค่าที่มีประเภท `T` เหมือนกัน" เวลาเรียกใช้ เราสามารถระบุประเภทลงไปชัดเจน (``) หรือปล่อยให้ TypeScript อนุมาน (infer) จากค่าที่ส่งเข้าไปก็ได้

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 ชัดเจนไม่กี่แบบ

Back to Blog | iCafe Forex | SiamLanCard | Siam2R