Với các object hằng như bảng route, feature flag hay giá trị mặc định cho biến môi trường, toán tử satisfies của TypeScript thường là lựa chọn tốt hơn so với khai báo kiểu (type annotation) hay ép kiểu bằng as. Nó kiểm tra giá trị theo đúng hình dạng bạn định nghĩa, nhưng vẫn giữ lại kiểu hẹp mà TypeScript tự suy ra, nhờ đó phần còn lại của code vẫn biết chính xác có những khoá nào và từng giá trị mang kiểu gì.
Toán tử này xuất hiện từ TypeScript 4.9, phát hành vào tháng 11 năm 2022. Nó đã đủ chín để dùng trong hầu hết codebase, vậy mà nhiều dự án vẫn khai báo kiểu cho object cấu hình theo thói quen. Dưới đây là lý do thói quen đó tốn kém hơn ta tưởng.
Ba cách gán kiểu cho một hằng số
Lấy ví dụ một bảng route nhỏ. Yêu cầu rất đơn giản: mỗi mục phải có path và cờ auth, và chỉ cần gõ sai ở bất kỳ đâu thì build phải báo lỗi.
Cách đầu tiên mà đa số chúng ta nghĩ tới là khai báo kiểu:
type Route = { path: string; auth: boolean };
const routes: Record<string, Route> = {
home: { path: "/", auth: false },
dashboard: { path: "/dashboard", auth: true },
};
routes.dashbord.path; // vẫn biên dịch: khoá string nào cũng được chấp nhận
Khai báo này có kiểm tra từng mục. Vấn đề nằm ở chỗ kiểu được khai báo đã thay thế kiểu được suy ra. Lúc này TypeScript tin rằng routes chấp nhận bất kỳ khoá string nào, nên chữ dashbord bị gõ sai vẫn biên dịch bình thường rồi mới vỡ lúc chạy (nếu bật noUncheckedIndexedAccess, bạn lại nhận cảnh báo "có thể undefined" trên mọi khoá, dù gõ đúng hay sai). Bạn cũng mất luôn keyof typeof routes, thứ lẽ ra cho bạn một union chính xác gồm tên các route.
Cách thứ hai là ép kiểu:
const fallback = { path: "/" } as Route; // vẫn biên dịch, dù thiếu auth
Type assertion về bản chất là bảo trình biên dịch: "cứ tin tôi". Tài liệu handbook của TypeScript giải thích rằng assertion được chấp nhận khi chuyển sang một phiên bản cụ thể hơn hoặc tổng quát hơn của một kiểu; chỉ những chuyển đổi "bất khả" mới bị từ chối. Thế nên thuộc tính bị thiếu vẫn lọt qua, trái ngược hoàn toàn với điều ta muốn ở một bước kiểm tra cấu hình.
Cách thứ ba là satisfies:
const routes = {
home: { path: "/", auth: false },
dashboard: { path: "/dashboard", auth: true },
} satisfies Record<string, Route>;
routes.dashbord; // lỗi: thuộc tính không tồn tại
type RouteName = keyof typeof routes; // "home" | "dashboard"
Mỗi mục vẫn được đối chiếu với Route. Nhưng biến giữ nguyên kiểu được suy ra, nên lỗi gõ sai bị bắt ngay, còn RouteName trở thành một union chính xác mà bạn có thể tái sử dụng cho link có kiểu hoặc các hàm hỗ trợ điều hướng.
satisfies thực sự kiểm tra những gì

satisfies chỉ đặt ra một câu hỏi: biểu thức này có gán được cho kiểu đã cho hay không? Nếu không, lỗi được báo ngay tại biểu thức. Nếu có, kiểu riêng của biểu thức được giữ nguyên và đi tiếp.
Có hai chi tiết khiến nó hữu ích hơn vẻ ngoài.
Thứ nhất, object literal vẫn được kiểm tra thuộc tính thừa (excess property check). Ghi chú phát hành TypeScript 4.9 dùng ví dụ một bảng màu có kiểu Record<Colors, ...>, trong đó khoá gõ sai như bleu bị bắt lỗi. Nghĩa là gõ nhầm tên khoá sẽ được báo, chứ không bị lặng lẽ chấp nhận.
Thứ hai, giá trị có kiểu union vẫn giữ được nhánh đã thu hẹp. Thử với giá trị mặc định cho môi trường:
type Env = Record<"API_URL" | "TIMEOUT_MS", string | number>;
const env = {
API_URL: "https://api.example.com",
TIMEOUT_MS: 5000,
} satisfies Env;
env.TIMEOUT_MS.toFixed(0); // ổn: được suy ra là number
Nếu dùng khai báo kiểu, TIMEOUT_MS sẽ là string | number, và bạn phải kiểm tra kiểu trước khi gọi toFixed. Với satisfies, trình biên dịch nhớ rằng đó là number. Còn nếu bỏ API_URL đi, build sẽ báo lỗi vì Record yêu cầu đủ cả hai khoá.
Giữ giá trị literal với as const satisfies

Đôi khi bạn cần chính các giá trị literal, chẳng hạn để rút ra một union gồm các HTTP method được phép. Kết hợp as const với satisfies sẽ làm được việc đó:
const methods = ["GET", "POST"] as const satisfies readonly string[];
type Method = (typeof methods)[number]; // "GET" | "POST"
Để ý chữ readonly trong kiểu đích. as const tạo ra một tuple readonly, mà mảng readonly thì không gán được cho string[] có thể thay đổi. Nếu vừa thêm as const mà gặp một lỗi khó hiểu, hãy kiểm tra xem kiểu đích có cần readonly hay không.
Khi nào satisfies là công cụ sai
Nó không phải thứ thay thế được mọi khai báo kiểu.
- Ranh giới module công khai. Khi bạn export một giá trị cho team khác dùng, kiểu được suy ra của nó trở thành một phần API của bạn. Nếu muốn bên sử dụng phụ thuộc vào một hợp đồng ổn định và rộng hơn, khai báo kiểu tường minh sẽ rõ ràng hơn, và cho phép bạn thay đổi phần bên trong mà không làm vỡ code của họ.
- Chữ ký hàm.
satisfiesáp dụng cho biểu thức. Tham số và kiểu trả về vẫn cần khai báo kiểu như bình thường. - Dữ liệu lúc chạy.
satisfiesbị xoá hoàn toàn khi biên dịch. Nó không thể kiểm tra JSON trả về từ API, một dòng dữ liệu từ database hayprocess.env. Với mọi thứ đi qua ranh giới tiến trình, hãy dùng thư viện kiểm tra lúc chạy như Zod, hoặc tự viết hàm kiểm tra.
Điểm cuối cùng là quan trọng nhất. satisfies bảo vệ những giá trị bạn viết trong mã nguồn. Nó không nói gì về những giá trị đến khi chương trình đang chạy.
Quy tắc thực dụng cho file cấu hình
Một nguyên tắc đơn giản sau đây phù hợp với phần lớn các team:
- Với object hằng định nghĩa trong chính code của bạn, như route, flag, design token hay bảng tra cứu, hãy dùng
satisfies. - Thêm
as constkhi cần giữ giá trị literal, và nhớ để kiểu đích làreadonly. - Khai báo kiểu cho giá trị được export khi bạn chủ ý muốn một hợp đồng công khai rộng hơn.
- Kiểm tra dữ liệu bên ngoài lúc chạy, đừng bao giờ chỉ trông vào một toán tử ở thời điểm biên dịch.
Lợi ích rất cụ thể: gõ sai tên khoá bị báo ngay trong editor, kiểu được thu hẹp giúp bỏ bớt những đoạn kiểm tra phòng thủ, và các union như RouteName có sẵn miễn phí từ chính dữ liệu bạn đã viết. Cái giá phải trả chỉ là một từ khoá đặt sau object, thay vì một kiểu đặt trước tên biến.




Đang tải bình luận…