Generic trong lập trình là gì? Ví dụ TypeScript cho tester

⏱︎

Read time:

3–4 minutes
Minh họa generic: nhiều kiểu dữ liệu dùng chung một cấu trúc

Hãy tưởng tượng Generic giống như một cái khuôn làm bánh. Bạn chỉ cần tạo ra một cái khuôn duy nhất, nhưng bạn có thể đổ bột mì để làm bánh mì, đổ sô-cô-la để làm bánh sô-cô-la, hoặc đổ thạch để làm bánh thạch…😅

Ví dụ
Khi tạo một danh sách lưu trữ List<T>, nếu bạn thay T bằng string, danh sách đó chỉ chứa chuỗi. Nếu bạn thay T bằng int, nó sẽ chứa số nguyên.

Ví dụ 1: Hàm “Trả đồ” (Hàm nhận gì trả nấy)

Giả sử bạn muốn viết một hàm nhận vào một giá trị và trả về chính giá trị đó.

❌ Cách làm thủ công (Không dùng Generic)

Nếu không dùng Generic, bạn phải viết rất nhiều hàm cho từng kiểu dữ liệu, hoặc dùng any (nhưng dùng any thì TypeScript sẽ không còn tác dụng kiểm tra lỗi nữa).

function echoNumber(item: number): number { return item; }
function echoString(item: string): string { return item; }

Cách làm thông minh với Generic

Bạn dùng một chữ đại diện (thường là chữ <T>) để đại diện cho “Kiểu dữ liệu sẽ truyền vào sau”.

// Chữ T ở đây đóng vai trò như một biến chứa Kiểu Dữ Liệu
function echo<T>(item: T): T {
    return item;
}

// Cách sử dụng:
let output1 = echo<string>("Xin chào các bạn"); // T bây giờ là string, hàm trả về string
let output2 = echo<number>(100);               // T bây giờ là number, hàm trả về number
let output3 = echo<boolean>(true);             // T bây giờ là boolean, hàm trả về boolean

Mổ xẻ 1 hàm generic

function echo<T>(item: T): T {
    return item;
}

/*
       1. KHAI BÁO BIẾN KIỂU
                     │
       function echo<T>(item: T): T { ... }
                              │   │
  2. ÁP KIỂU CHO ĐẦU VÀO ─────┘   └───── 3. ÁP KIỂU CHO ĐẦU RA

*/

1. Chữ T thứ nhất: <T> (Khai báo tham số kiểu)

  • Vị trí: Ngay sau tên hàm echo.
  • Vai trò: Đây là nơi khai báo (đăng ký) với TypeScript rằng: “Hàm này là một hàm Generic, và tôi sẽ dùng chữ cái T này làm đại diện cho kiểu dữ liệu sẽ được truyền vào sau này”.
  • Hình ảnh thực tế: Nó giống như việc bạn đặt tên cho một biến số (ví dụ: x), nhưng ở đây T là một biến chứa kiểu dữ liệu (Type Variable). Bạn có thể đổi chữ T này thành bất kỳ chữ gì khác như <U>, <K>, <Type> đều được, miễn là các chữ sau đổi theo. Tuy nhiên nên để theo convention là chữ T

2. Chữ T thứ hai: item: T (Kiểu dữ liệu của tham số đầu vào)

  • Vị trí: Bên trong dấu ngoặc đơn, đi kèm với tham số item.
  • Vai trò: Quy định kiểu dữ liệu của đầu vào. Nghĩa là tham số item bắt buộc phải có kiểu dữ liệu trùng với kiểu T đã khai báo ở trên.
  • Cách hoạt động: Khi bạn gọi hàm echo("Hello"), TypeScript thấy đầu vào là một chuỗi (string). Nó sẽ lập tức gán giá trị string cho biến T. Lúc này, item sẽ có kiểu là string.

3. Chữ T thứ ba: : T (Kiểu dữ liệu của giá trị trả về)

  • Vị trí: Sau dấu hai chấm : của hàm (trước dấu mở ngoặc nhọn {).
  • Vai trò: Quy định kiểu dữ liệu của đầu ra (giá trị mà hàm trả về). Nó cam kết rằng hàm này sau khi xử lý xong sẽ trả về một kết quả có đúng kiểu dữ liệu T.
  • Ý nghĩa: Nhờ chữ T này, TypeScript biết chắc chắn rằng nếu bạn truyền vào một string (item: T), bạn sẽ nhận lại chính xác một string (: T), chứ không phải là number hay boolean.

Tóm gọn lại

  • Khi nào thấy dấu <T> (hoặc <U>, <K>), đó chính là Generic.
  • Nó là một “biến chứa kiểu dữ liệu”. Bạn truyền kiểu dữ liệu nào vào, toàn bộ chỗ nào có chữ T sẽ biến thành kiểu dữ liệu đó.
  • Nó giúp code vừa linh hoạt (dùng được cho nhiều kiểu) vừa an toàn (không lo bị sai kiểu dữ liệu lúc chạy).