← TypeScript Thực Chiến

Bài 6 · Vận dụng · 24 phút

Generics

Biên soạn bởi Nguyễn Anh Tuấn

Generics - hàm và kiểu tham số hoá để tái dùng mà vẫn an toàn kiểu; ràng buộc generic (extends), tham số kiểu mặc định và suy luận tham số kiểu khi gọi hàm. Vì sao generics giống "hàm cho kiểu" và là nền của mọi thư viện TS chuyên nghiệp.

Giả sử mèo con viết hàm "lấy phần tử đầu của mảng". Nếu nhận any[], hàm dùng được cho mọi mảng, nhưng kết quả là any - mất sạch kiểu, hết an toàn. Nếu viết riêng cho từng kiểu thì lặp code. Generics giải bài toán này: tham số hoá kiểu, để hàm tái dùng được mà vẫn giữ kiểu:

any (mất kiểu) vs generic (giữ kiểu)

function dauTienAny(arr: any[]): any {
  return arr[0];
}
const x = dauTienAny([1, 2, 3]);   // x: any -> mat thong tin

function dauTien<T>(arr: T[]): T {
  return arr[0];
}
const y = dauTien([1, 2, 3]);      // y: number -> TS suy ra T = number
  • any cho dùng cho mọi kiểu nhưng làm mất thông tin kiểu ở đầu ra.
  • Generic <T> là một "ô trống" cho kiểu, được điền khi gọi hàm.
  • Nhờ T, đầu vào và đầu ra được nối kiểu với nhau - tái dùng mà vẫn an toàn.

Đặt <T> trước danh sách tham số là khai báo một tham số kiểu. Điều hay nhất: thường mèo con không phải ghi T khi gọi - TypeScript tự suy ra từ giá trị truyền vào, đúng tinh thần suy luận kiểu ở Bài 2:

ghep-doi.ts

function ghepDoi<T>(x: T): [T, T] {
  return [x, x];
}
console.log(ghepDoi("bo"), ghepDoi(7));
// ghepDoi("bo") -> [string, string]; ghepDoi(7) -> [number, number]

Kết quả khi chạy

[ 'bo', 'bo' ] [ 7, 7 ]
  • <T> trước tham số khai báo một tham số kiểu cho hàm.
  • TypeScript thường tự suy ra T từ đối số, không cần ghi <T> khi gọi.
  • Kết quả mang đúng kiểu cụ thể theo cái đã truyền vào.

Một <T> tự do thì trong hàm bạn gần như không làm gì được với nó (vì T có thể là bất cứ kiểu nào). Ràng buộc bằng extends nói "T phải có ít nhất hình dạng này", mở khoá cho bạn dùng phần đó an toàn:

rang-buoc.ts

function doDai<T extends { length: number }>(x: T): number {
  return x.length;   // an toan: T chac chan co length
}
doDai("meo");        // OK - string co length
doDai([1, 2, 3]);    // OK - array co length
doDai(42);           // Loi - number khong co length

tsc báo

error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
  • <T extends Hinh> giới hạn T phải khớp (là con của) một hình dạng.
  • Trong hàm, bạn được dùng các phần mà ràng buộc bảo đảm có.
  • Truyền giá trị không khớp ràng buộc thì tsc báo lỗi ngay (TS2345).

Không chỉ hàm, cả interface, type và class đều nhận tham số kiểu. Và một tham số kiểu có thể có giá trị mặc định (default type parameter) để khi không ghi thì dùng cái mặc định:

generic-type.ts

interface Hop<T> {
  giaTri: T;
}
const h: Hop<string> = { giaTri: "bo" };

type DanhSach<T = string> = T[];   // mac dinh T = string
const ds: DanhSach = ["a", "b"];   // khong ghi <...> -> T la string

console.log(h.giaTri, ds.length);

Kết quả khi chạy

bo 2
  • interface/type/class đều có thể nhận tham số kiểu, vd Hop<T>, Mang<T>.
  • Tham số kiểu mặc định (T = string) cho phép bỏ qua khi không cần đổi.
  • Generics là cách các thư viện (Array<T>, Promise<T>, Map<K, V>) hoạt động.

Generics chính là "hàm cho kiểu": bạn truyền kiểu vào như truyền giá trị, và nhận lại một kiểu mới. Đây là nền của mọi thư viện TypeScript chuyên nghiệp. Từ đây, ta bước vào phần thú vị nhất: lập trình hẳn ở tầng kiểu.

Bài tiếp theo: Conditional types

Nếu generics là "hàm cho kiểu", thì bài sau là "if cho kiểu": conditional types và infer - cho phép một kiểu thay đổi tuỳ theo kiểu đầu vào. Đây là bước TypeScript trở thành một ngôn ngữ ở tầng kiểu.

Câu hỏi thường gặp

any vứt bỏ kiểm tra kiểu - dauTienAny([1,2,3]) trả về any, bạn mất sạch thông tin. Generics thì GIỮ và NỐI kiểu: dauTien([1,2,3]) trả về number vì TypeScript suy ra T = number. Generic an toàn; any thì không.

Không. Thường TypeScript tự suy luận T từ tham số bạn truyền vào, nên dauTien([1,2,3]) là đủ. Bạn chỉ cần ghi rõ <T> khi TS không đoán được, hoặc khi muốn ép một kiểu cụ thể khác với cái nó đoán.

Nó nói "T phải khớp (là con của) kiểu này". <T extends { length: number }> nghĩa là T có thể là bất cứ kiểu nào MIỄN LÀ có field length kiểu number. Nhờ ràng buộc đó, trong hàm bạn được phép dùng x.length một cách an toàn.

Quy ước hay gặp là chữ cái đơn: T (type), K (key), V (value), E (element). Khi một generic có nhiều tham số kiểu hoặc nghĩa quan trọng, cứ đặt tên rõ ràng (vd TData, TError) cho dễ đọc - không có luật bắt buộc.

Không. Như mọi thứ thuộc hệ thống kiểu, generics bị xoá khi biên dịch (type erasure, Bài 1). Lúc chạy chỉ còn JavaScript thường, không có tham số kiểu nào. Generics thuần tuý là công cụ an toàn lúc viết code.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Generics khác any ở điểm cốt lõi nào?

Bài tập về nhà

  1. 1

    any làm mất kiểu

    Viết một hàm lấy phần tử đầu của mảng kiểu any[] trả về any. Gán kết quả cho một biến rồi thử gọi một method - xem TS có cảnh báo gì không.

    ✅ Hoàn thành khi: Chứng minh kết quả là any: gọi method bừa cũng không bị TS cản, đúng kiểu "mất lưới an toàn".

  2. 2

    Generic giữ kiểu

    Viết lại hàm trên thành generic dauTien<T>(arr: T[]): T. Gọi với mảng số và mảng chuỗi, soi kiểu TS suy ra cho kết quả.

    ✅ Hoàn thành khi: Kết quả có kiểu cụ thể (number với mảng số, string với mảng chuỗi), không còn là any.

  3. 3

    Để TS suy luận T

    Viết một generic function nhận một giá trị và trả về một cặp [T, T]. Gọi nó mà KHÔNG ghi <T>, kiểm tra TS vẫn suy ra đúng.

    ✅ Hoàn thành khi: Hàm gọi không cần <T>, kết quả có kiểu tuple đúng (vd [string, string] khi truyền chuỗi).

  4. 4

    Ràng buộc bằng extends

    Viết một generic function chỉ nhận thứ có field length, dùng <T extends { length: number }>. Thử truyền một chuỗi, một mảng, và một số.

    ✅ Hoàn thành khi: Chuỗi và mảng biên dịch sạch; số bị tsc báo lỗi (TS2345) vì không có length.

  5. 5

    Generic type với default

    Viết một generic interface hoặc type có một tham số kiểu, và một type alias generic có tham số kiểu MẶC ĐỊNH.

    ✅ Hoàn thành khi: Dùng được generic type với kiểu cụ thể, và dùng được type có default mà không cần ghi tham số kiểu.

  6. 6

    Cần generic hay không?

    Nêu một tình huống generic thực sự đáng dùng (tái dùng cho nhiều kiểu) và một tình huống KHÔNG nên generic (chỉ một kiểu, generic làm rối).

    ✅ Hoàn thành khi: Hai ví dụ kèm lý do: vì sao chỗ này generic xứng đáng, vì sao chỗ kia một kiểu cụ thể là đủ.