Giới thiệu

TypeScript mang lại khả năng kiểm tra kiểu tĩnh cho JavaScript, giúp giảm lỗi runtime. Một trong những tính năng mạnh mẽ nhất của TypeScript là Generics. Generics cho phép bạn viết các hàm, lớp, interface có thể hoạt động với nhiều kiểu dữ liệu khác nhau mà vẫn giữ được tính an toàn của kiểu.

Generics là gì?

Generics là một cơ chế truyền tham số kiểu (type parameter) vào một hàm hoặc một cấu trúc dữ liệu. Thay vì cố định một kiểu cụ thể, bạn định nghĩa một T (hoặc nhiều hơn) và để người dùng quyết định kiểu thực tế khi gọi.

Cách khai báo Generics trong hàm

Để khai báo một hàm generic, bạn đặt danh sách các tham số kiểu trong dấu <> ngay sau tên hàm.

function identity<T>(value: T): T {
    return value;
}

// Sử dụng hàm generic
const num = identity<number>(42);
const str = identity<string>('hello');

Trong ví dụ trên, hàm identity nhận bất kỳ kiểu nào và trả về cùng kiểu đó, giúp TypeScript suy ra kiểu trả về chính xác.

Generics trong lớp và interface

Bạn có thể áp dụng Generics cho lớp để tạo các cấu trúc dữ liệu tái sử dụng.

class Stack<T> {
    private items: T[] = [];

    push(item: T): void {
        this.items.push(item);
    }

    pop(): T | undefined {
        return this.items.pop();
    }
}

// Tạo stack cho số và chuỗi
const numberStack = new Stack<number>();
numberStack.push(10);
const stringStack = new Stack<string>();
stringStack.push('a');

Interface cũng có thể nhận Generics, giúp mô tả các API linh hoạt.

interface ApiResponse<T> {
    data: T;
    status: number;
    error?: string;
}

// Ví dụ trả về danh sách người dùng
type User = { id: number; name: string };
const response: ApiResponse<User[]> = {
    data: [{ id: 1, name: 'Alice' }],
    status: 200,
};

Kết hợp Generics với Constraints

Đôi khi bạn muốn giới hạn kiểu truyền vào để có thể sử dụng các thuộc tính nhất định. Điều này được thực hiện bằng constraints (ràng buộc).

function getLength<T extends { length: number }>(arg: T): number {
    return arg.length;
}

getLength('hello'); // 5
getLength([1, 2, 3]); // 3
// getLength(10); // Lỗi: number không có thuộc tính length

Ràng buộc { length: number } đảm bảo rằng đối tượng truyền vào có thuộc tính length.

Một ví dụ thực tế: API client type‑safe

Giả sử bạn xây dựng một client gọi API REST và muốn đảm bảo kiểu dữ liệu trả về luôn đúng. Bạn có thể định nghĩa một hàm generic nhận kiểu dữ liệu mong muốn.

async function fetchJson<T>(url: string): Promise<ApiResponse<T>> {
    const response = await fetch(url);
    const data = await response.json();
    return {
        data: data as T,
        status: response.status,
    };
}

// Sử dụng với kiểu User
type User = { id: number; name: string };
fetchJson<User[]>('/api/users')
    .then(res => console.log(res.data))
    .catch(err => console.error(err));

Nhờ Generics, hàm fetchJson trả về Promise<ApiResponse<User[]>>, giúp IDE gợi ý và phát hiện lỗi khi truy cập thuộc tính.

Kết luận

Generics là công cụ không thể thiếu để viết mã TypeScript linh hoạt, tái sử dụng và an toàn. Khi kết hợp với constraints, chúng còn giúp bạn kiểm soát chặt chẽ các kiểu dữ liệu đầu vào, giảm thiểu lỗi runtime. Để nắm vững và áp dụng Generics trong các dự án thực tế, bạn có thể Tham khảo khóa học "Lập trình TypeScript từ cơ bản đến nâng cao" tại đây.