Generic function: tái sử dụng kiểu mà vẫn giữ an toàn
Generic cho phép viết hàm/class/type hoạt động trên nhiều kiểu mà vẫn giữ quan hệ kiểu giữa input và output — thay vì mất an toàn bằng any. Một hàm identity<T>(x: T): T trả về đúng kiểu nhận vào; một Repository<T> thao tác trên bất kỳ entity nào mà vẫn biết kiểu cụ thể. Đây là công cụ chính để viết thư viện và utility dùng lại được mà không hy sinh type-safety. Lạm dụng — generic lồng nhiều tầng, constraint phức tạp — lại tạo ra ký hiệu khó đọc không kém gì mất kiểu.
Cơ chế hoạt động
Generic là "tham số kiểu": kiểu được truyền vào như đối số, suy ra từ cách gọi. Constraint (extends) giới hạn kiểu được phép truyền, mở khóa truy cập property an toàn bên trong:
function first<T>(arr: T[]): T | undefined {
return arr[0] // trả đúng kiểu phần tử, suy ra từ arr
}
const n = first([1, 2, 3]) // T = number -> n: number | undefined
const s = first(['a', 'b']) // T = string -> s: string | undefined
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key] // K bị ràng buộc là key của T -> obj[key] an toàn, trả đúng kiểu giá trị
}
K extends keyof T là constraint: nó đảm bảo key là một property có thật của obj, nên pluck(user, 'emial') lỗi compile, và kiểu trả về chính xác là kiểu của field đó. So với any, generic giữ nguyên mối liên hệ kiểu thay vì xóa nó.
Vấn đề gặp trong production
Use case điển hình: repository/cache dùng chung. Một lớp generic thao tác trên nhiều entity mà mỗi chỗ dùng vẫn có kiểu cụ thể:
class Repository<T extends { id: string }> {
private store = new Map<string, T>()
get(id: string): T | undefined { return this.store.get(id) }
save(entity: T): void { this.store.set(entity.id, entity) } // constraint đảm bảo có id
}
const users = new Repository<User>()
users.save(someUser)
const u = users.get('1') // u: User | undefined, không phải any
Constraint T extends { id: string } đảm bảo mọi entity dùng repo này đều có id, nên save truy cập entity.id an toàn. Không có generic, hoặc phải viết lại repo cho từng entity (lặp), hoặc dùng any (mất kiểu).
Failure mode: generic phức tạp khó đọc. Vì TS cho phép constraint lồng, conditional type, và nhiều tham số kiểu, dễ viết ra chữ ký như function f<T extends Record<string, U>, U extends keyof T, V = T[U]>(...) mà không ai trong nhóm giải mã nổi. Thông báo lỗi cho generic phức tạp cũng dài và khó hiểu. Dấu hiệu lạm dụng: phải dừng lại vẽ sơ đồ để hiểu một chữ ký hàm, hoặc thêm tham số kiểu chỉ để "phòng xa" mà chưa có nhu cầu thật.
Failure mode: generic không cần thiết. Đôi khi một hàm được khai báo generic nhưng tham số kiểu chỉ dùng một lần — lúc đó nó không thêm an toàn gì so với một kiểu cụ thể hoặc một union, chỉ thêm nhiễu. Generic chỉ đáng khi giữ quan hệ giữa nhiều vị trí (input↔output, nhiều tham số), không phải để trông "tổng quát".
Cách debug và monitor
Khi một generic suy ra kiểu sai hoặc quá rộng (ra unknown/{}), hover trong editor để xem TS suy T thành gì tại chỗ gọi — thường constraint thiếu hoặc TS không có đủ thông tin để suy, cần truyền tham số kiểu tường minh. Thông báo lỗi dài cho generic phức tạp là tín hiệu nên đơn giản hóa chữ ký. Theo dõi thời gian compile: generic với conditional/recursive nặng làm type checker chậm; --extendedDiagnostics cho biết check tốn ở đâu. Quy ước review: một chữ ký generic phải đọc hiểu được trong vài giây, nếu không thì tách nhỏ hoặc thay bằng kiểu cụ thể.
Tradeoff
Generic tăng tái sử dụng mà không mất an toàn — một utility/lớp dùng cho nhiều kiểu nhưng mỗi chỗ dùng vẫn có kiểu chính xác, thay cho lựa chọn lặp code (viết lại cho từng kiểu) hoặc any (mất kiểm tra). Cái giá là độ phức tạp: generic lồng nhiều tầng và constraint phức tạp khó đọc, lỗi khó hiểu, compile chậm. Quy tắc thực tế: dùng generic khi cần giữ quan hệ kiểu giữa các vị trí (input↔output, repository, cache, hàm bậc cao); thêm constraint (extends) để mở khóa truy cập an toàn và bắt lỗi sớm; nhưng giữ chữ ký đơn giản, không thêm tham số kiểu "phòng xa", và đơn giản hóa ngay khi chữ ký bắt đầu khó đọc.
Câu hỏi phỏng vấn
Generic hoạt động ra sao, và nó giải quyết vấn đề gì mà
anykhông làm được?
Generic là tham số kiểu được truyền vào hàm/class/type và thường được TS suy ra từ cách gọi; constraint (T extends ...) giới hạn kiểu hợp lệ và mở khóa truy cập property an toàn bên trong. Khác với any — vốn xóa hoàn toàn thông tin kiểu — generic giữ nguyên quan hệ giữa input và output: first<T>(arr: T[]): T | undefined trả đúng kiểu phần tử, pluck<T, K extends keyof T> đảm bảo key tồn tại và trả đúng kiểu field. Nhờ vậy một utility/repository dùng được cho nhiều kiểu mà mỗi chỗ gọi vẫn có kiểu cụ thể, không phải chọn giữa lặp code và mất an toàn. Điểm ăn điểm: dùng constraint để bắt lỗi sớm, chỉ dùng generic khi cần giữ quan hệ kiểu (không phải để trông tổng quát), và cảnh báo generic lồng phức tạp gây lỗi khó đọc + compile chậm nên cần giữ chữ ký đơn giản.
Hands-on
Viết một Repository<T extends { id: string }> generic dùng được cho nhiều entity thật (User, Order) và xác nhận mỗi instance giữ kiểu cụ thể (get trả User | undefined, không phải any), đồng thời constraint chặn entity thiếu id ngay lúc compile. Viết một hàm pluck với K extends keyof T và thử truy cập một key sai để thấy lỗi compile cùng kiểu trả về chính xác. Sau đó cố tình viết một chữ ký generic nhiều tham số kiểu lồng nhau với conditional type tới mức lỗi và inference trở nên khó đọc, rồi refactor về một chữ ký đơn giản hơn — so sánh độ dễ đọc, chất lượng thông báo lỗi, và thời gian compile.
Top comments (0)