Giới thiệu
Trong các dự án JavaScript lớn, việc truyền dữ liệu giữa các tầng thường gặp lỗi kiểu dữ liệu khiến runtime gặp sự cố. TypeScript cung cấp cơ chế Generics giúp chúng ta mô tả các kiểu một cách linh hoạt, đồng thời giữ được tính an toàn khi biên dịch. Bài viết sẽ đi sâu vào cách khai báo và áp dụng Generics trong hàm, interface và trong việc xây dựng một client API type‑safe.
Generics là gì và tại sao cần dùng
Generics cho phép chúng ta viết các hàm hoặc cấu trúc dữ liệu mà không cần xác định kiểu cụ thể ngay từ đầu. Thay vào đó, kiểu sẽ được quyết định khi hàm hoặc đối tượng được sử dụng. Điều này giúp giảm việc lặp lại mã, đồng thời tận dụng tối đa khả năng kiểm tra kiểu của TypeScript.
Định nghĩa cơ bản
Ví dụ đơn giản nhất là hàm identity trả về đúng giá trị đầu vào mà không thay đổi kiểu.
function identity<T>(value: T): T {
return value;
}
// Sử dụng
const num = identity<number>(42);
const str = identity<string>('Hello');
Ở đây, T là một placeholder cho kiểu dữ liệu. Khi gọi hàm, TypeScript sẽ suy ra hoặc chúng ta có thể chỉ định kiểu cụ thể.
Áp dụng Generics trong hàm
Thực tế, chúng ta thường cần truy cập thuộc tính của một đối tượng mà không biết trước tên thuộc tính. Generics kết hợp với keyof giúp giải quyết vấn đề này.
function getProperty<Obj, Key extends keyof Obj>(obj: Obj, key: Key): Obj[Key] {
return obj[key];
}
const user = { id: 1, name: 'Alice', age: 30 };
const userName = getProperty(user, 'name'); // kiểu string
const userId = getProperty(user, 'id'); // kiểu number
Nhờ Key extends keyof Obj, TypeScript chỉ cho phép truyền vào các khóa hợp lệ, tránh lỗi typo.
Generics với interface và type
Chúng ta có thể định nghĩa một Repository chung cho mọi thực thể, chỉ cần truyền kiểu thực thể vào.
interface Repository<T> {
findById(id: number): Promise<T | null>;
findAll(): Promise<T[]>;
save(entity: T): Promise<T>;
}
// Triển khai cho thực thể User
type User = {
id: number;
name: string;
email: string;
};
class UserRepository implements Repository<User> {
async findById(id: number) {
// giả lập fetch dữ liệu
return { id, name: 'Demo', email: '[email protected]' };
}
async findAll() {
return [];
}
async save(entity: User) {
return entity;
}
}
Nhờ Generics, chúng ta có thể tái sử dụng cùng một interface cho các thực thể khác như Product, Order mà không viết lại toàn bộ phương thức.
Thực hành: Tạo API client type‑safe
Giả sử chúng ta xây dựng một client để gọi API trả về danh sách người dùng. Sử dụng Generics, chúng ta có thể định nghĩa một hàm request nhận kiểu phản hồi làm tham số.
async function request<ResponseType>(url: string, init?: RequestInit): Promise<ResponseType> {
const res = await fetch(url, init);
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const data = await res.json();
return data as ResponseType;
}
// Định nghĩa kiểu dữ liệu trả về
type UserDTO = {
id: number;
username: string;
avatarUrl: string;
};
// Gọi API
(async () => {
const users = await request<UserDTO[]>('https://api.example.com/users');
console.log(users[0].username); // TypeScript biết rằng users là mảng UserDTO
})();
Với cách tiếp cận này, bất kỳ hàm nào gọi request đều nhận được kiểu phản hồi chính xác, giảm thiểu lỗi khi truy cập thuộc tính không tồn tại.
Kết luận
Generics là công cụ mạnh mẽ giúp viết mã TypeScript linh hoạt, tái sử dụng và an toàn. Khi áp dụng đúng cách, chúng ta có thể xây dựng các API client, repository và các hàm tiện ích mà không lo về lỗi kiểu dữ liệu. Để nắm vững hơn về Generics và các tính năng nâng cao của TypeScript, Tham khảo khóa học "Lập trình TypeScript từ cơ bản đến nâng cao" tại đây.



.png)




