Giới thiệu về Generic trong TypeScript
Generic là một trong những tính năng mạnh mẽ nhất của TypeScript, cho phép bạn viết các hàm, lớp hoặc 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 tĩnh. Thay vì lặp lại cùng một logic cho các kiểu riêng lẻ, Generic giúp tái sử dụng code một cách hiệu quả, giảm thiểu lỗi và cải thiện khả năng bảo trì.
Lợi ích khi sử dụng Generic
Việc áp dụng Generic mang lại một số lợi ích thiết thực:
- Tái sử dụng code: Một hàm hoặc lớp có thể làm việc với bất kỳ kiểu dữ liệu nào mà không cần viết lại.
- Kiểm tra kiểu tại thời điểm biên dịch: Trình biên dịch sẽ phát hiện lỗi kiểu ngay khi viết code, giảm thiểu lỗi runtime.
- Đọc mã dễ hiểu hơn: Khi khai báo rõ ràng các tham số kiểu, người đọc có thể nắm bắt được mục đích và cách sử dụng nhanh chóng.
Định nghĩa kiểu dữ liệu tổng quát
Để khai báo một hàm generic, bạn chỉ cần đặt tên cho kiểu tham số trong dấu ngoặc nhọn sau tên hàm. Ví dụ dưới đây minh họa một hàm identity trả về đúng giá trị đầu vào.
function identity<T>(value: T): T {
return value;
}
// Sử dụng hàm generic
let num = identity<number>(42);
let text = identity<string>('Hello');Ở đây, T là một placeholder cho bất kỳ kiểu nào mà người gọi hàm cung cấp.
Generic với nhiều tham số
Khi cần làm việc với nhiều kiểu đồng thời, bạn có thể khai báo nhiều tham số generic. Dưới đây là một hàm merge kết hợp hai đối tượng lại với nhau.
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const result = merge<{id: number}, {name: string}>({id: 1}, {name: 'Alice'});
// result có kiểu {id: number; name: string;}Việc sử dụng T & U tạo ra một kiểu giao nhau, cho phép truy cập cả hai thuộc tính mà không cần ép kiểu.
Áp dụng Generic trong các trường hợp thực tế
Ví dụ: Hàm filter mảng
Thay vì viết riêng hàm filter cho mỗi kiểu phần tử, chúng ta có thể tạo một hàm generic nhận vào một mảng và một hàm kiểm tra.
function filterArray<T>(arr: T[], predicate: (item: T) => boolean): T[] {
const result: T[] = [];
for (const item of arr) {
if (predicate(item)) {
result.push(item);
}
}
return result;
}
const numbers = filterArray<number>([1, 2, 3, 4], n => n % 2 === 0);
const users = filterArray<{name: string; age: number}>(
[{name: 'Bob', age: 30}, {name: 'Anna', age: 22}],
u => u.age > 25
);
Hàm này hoạt động với bất kỳ kiểu dữ liệu nào, đồng thời vẫn giữ được kiểm tra kiểu chặt chẽ.
Ví dụ: Repository pattern
Trong các dự án Node.js hoặc NestJS, việc xây dựng một lớp repository chung cho các thực thể là một cách tiếp cận phổ biến. Generic giúp định nghĩa một lớp cơ sở có thể tái sử dụng cho mọi entity.
class BaseRepository<Entity> {
private items: Entity[] = [];
create(item: Entity): void {
this.items.push(item);
}
findAll(): Entity[] {
return this.items;
}
}
interface User {
id: number;
name: string;
}
const userRepo = new BaseRepository<User>();
userRepo.create({id: 1, name: 'John'});
const allUsers = userRepo.findAll();
Nhờ Generic, lớp BaseRepository không cần biết trước kiểu thực thể nào, nhưng vẫn cung cấp đầy đủ các phương thức CRUD cơ bản.
Kỹ thuật nâng cao: Conditional Types và Infer
TypeScript 2.8 giới thiệu Conditional Types, cho phép tạo ra các kiểu phụ thuộc vào một điều kiện. Khi kết hợp với infer, bạn có thể trích xuất kiểu từ một hàm hoặc một cấu trúc phức tạp.
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function hello(name: string) {
return `Hello, ${name}`;
}
type HelloReturn = ReturnType<typeof hello>; // string
Trong ví dụ trên, ReturnType sẽ trả về kiểu trả về của hàm được truyền vào, giúp bạn xây dựng các utility type mạnh mẽ mà không cần viết lại.
Kết luận
Generic là công cụ không thể thiếu khi bạn muốn viết TypeScript sạch, tái sử dụng và an toàn. Từ các hàm đơn giản đến các pattern kiến trúc phức tạp, việc nắm vững Generic sẽ giúp bạn giảm thiểu lỗi và tăng tốc độ phát triển. Để nâng cao kỹ năng và áp dụng sâu hơn, Tham khảo khóa học "Lập trình TypeScript từ cơ bản đến nâng cao" tại đây.
.png)







