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
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.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Generics khác any ở điểm cốt lõi nào?
- 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
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
Để 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
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
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
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à đủ.