TypeScript’s new const type parameter syntax lets a function keep literal types intact without forcing callers to sprinkle as const everywhere, cutting the most common source of type-widening bugs.
Vấn đề mở rộng kiểu (widening) luôn ám ảnh mã nguồn generic
Khi một hàm generic nhận một object literal, trình biên dịch sẽ mở rộng bất kỳ thuộc tính literal nào thành kiểu nguyên thủy (primitive type) rộng hơn của nó.
function call<T>(arg: T) {}
call({ method: "GET" }) // T is inferred as { method: string }
Giá trị literal "GET" bị thu hẹp thành string. Các mã nguồn phía sau phụ thuộc vào giá trị chính xác—chẳng hạn như discriminated unions hoặc trích xuất template-literal—sẽ bị lỗi vì kiểu dữ liệu không còn mang giá trị literal chính xác nữa. Từ lâu, các nhà phát triển đã phải xử lý vấn đề này bằng cách viết { method: "GET" } as const tại nơi gọi hàm, điều này báo cho trình biên dịch giữ nguyên giá trị literal, nhưng cách khắc phục đó nằm ở phía người gọi chứ không phải ở định nghĩa của hàm.
Tham số kiểu const: Giải pháp ở cấp độ chữ ký (signature)
Modifier const mới trên một tham số kiểu sẽ báo cho trình biên dịch suy luận ra kiểu hẹp nhất có thể cho đối số generic đó. Việc khai báo một hàm là function foo<const T>(arg: T) sẽ khiến T tự động hoạt động như thể người gọi đã viết as const.
- Các giá trị literal string, number, boolean vẫn giữ nguyên giá trị chính xác của chúng (
"GET"thay vìstring). - Các mảng (Arrays) trở thành các tuple readonly với mỗi phần tử được định nghĩa kiểu chính xác.
- Các đối tượng (Objects) trở thành các cấu trúc readonly sâu (deeply readonly), bảo toàn các kiểu literal ở mọi cấp độ lồng nhau.
Vì ràng buộc này nằm ngay trong chữ ký (signature) của hàm, mọi người gọi đều được hưởng lợi một cách tự động; việc quên ép kiểu không còn là nguyên nhân dẫn đến sự thiếu an toàn về kiểu nữa.
Tại sao nó vượt trội hơn thủ thuật as const truyền thống
as const là một giải pháp ở phía người gọi (caller-side). Nó yêu cầu mọi người sử dụng hàm generic phải nhớ thêm phần khẳng định (assertion). Chỉ cần thiếu một lần gọi, tính an toàn về kiểu sẽ biến mất. Tham số kiểu const chuyển trách nhiệm này vào chính thiết kế API: hàm sẽ tuyên bố "Tôi cần hình dạng hẹp nhất của bất cứ thứ gì bạn truyền vào", và trình biên dịch sẽ thực thi điều đó.
Sự thay đổi đó đặc biệt quan trọng đối với các thư viện và tiện ích cung cấp các bộ dựng (builders) generic, các factory cấu hình, hoặc bất kỳ API nào mà giá trị literal của một trường quyết định logic về kiểu. Tác giả thư viện có thể đảm bảo việc suy luận chính xác mà không cần phải kiểm soát mã nguồn của người dùng.
Các kịch bản thực tế được hưởng lợi
- Các bộ dựng cấu hình (Configuration builders) – tên môi trường (
"dev" | "prod") vẫn là literal, cho phép kiểm tra discriminated-union mà không cần ép kiểu thêm. - Định nghĩa route API – các chuỗi đường dẫn được giữ nguyên chính xác, cho phép các kiểu template-literal trích xuất tham số (
"/users/:id"→\/users/${string}``). - Các công cụ hỗ trợ máy trạng thái (State-machine helpers) – các định danh trạng thái vẫn là các literal cố định thông qua việc gọi chuỗi phương thức (method chaining), ngăn chặn việc nhầm lẫn trạng thái ngoài ý muốn.
Trong mỗi trường hợp, tham số const giúp loại bỏ mã lặp lại (boilerplate) as const và giảm khả năng xảy ra các lỗi tinh vi.
Kết hợp với toán tử satisfies
Toán tử satisfies xác thực rằng một giá trị tuân thủ một kiểu cấu trúc (structural type) trong khi vẫn bảo toàn thông tin literal ban đầu của nó. Sử dụng cả hai cùng lúc sẽ mang lại lợi ích tối ưu: tham số const cung cấp khả năng suy luận hẹp, và satisfies đảm bảo giá trị đáp ứng đúng hình dạng yêu cầu.
function makeConfig<const C>(cfg: C) {
// cfg is inferred with exact literals
}
const cfg = {
env: "staging",
ports: [8080, 8443],
} satisfies { env: string; ports: number[] };
makeConfig(cfg); // works, literals stay intact
Khi nào nên tiếp tục dùng as const
Tham số const phát huy tác dụng tốt nhất khi bạn kiểm soát được chữ ký của hàm. Nếu bạn đang làm việc với các hàm của bên thứ ba không có modifier này, hoặc bạn cần bảo toàn literal một lần cho một biến cục bộ, as const vẫn là công cụ phù hợp. Nó vẫn đóng vai trò là cách mặc định để đóng băng một giá trị mà không làm thay đổi API được gọi.
Những điều cần lưu ý tiếp theo
Tính năng này vẫn còn mới, vì vậy các công cụ và mô hình cộng đồng đang dần phát triển. Hãy mong đợi các bản cập nhật hỗ trợ IDE để hiển thị cú pháp mới trong tính năng tự động hoàn thành (autocomplete) và các gợi ý sửa lỗi nhanh (quick-fix). Hãy chú ý đến những người duy trì thư viện: nhiều người sẽ bắt đầu chuyển đổi các generic công khai sang tham số const, điều này có thể gây ra các thay đổi gây lỗi (breaking changes) cho những mã nguồn trước đây vốn dựa vào việc ép kiểu as const rõ ràng.
Điểm mấu chốt: Bằng cách nhúng việc bảo toàn literal trực tiếp vào các tham số kiểu của hàm, các tham số kiểu const của TypeScript loại bỏ một nguồn gây lỗi mở rộng kiểu phổ biến và chuyển trách nhiệm an toàn từ người gọi về lại cho người thiết kế API. Hãy sử dụng chúng cho bất kỳ điểm truy cập generic nào mà bạn sở hữu; hãy dành as const cho các giá trị cục bộ hoặc các API bên ngoài.
