TypeScript giúp bắt các lỗi ngớ ngẩn trước khi chúng đến production. Nó nhắc nhở bạn khi viết sai thuộc tính, quên tham số, hay khi một nhánh trả về sai cấu trúc. Bạn sửa lỗi, build thành công, và ship sản phẩm. Nhưng các kiểu tĩnh (static types) có một giới hạn cứng. Một khi trình biên dịch hoàn tất, mọi annotation đều bị loại bỏ. Engine JavaScript chạy mã của bạn chưa bao giờ nghe nói về các interface, branded types, hay các string literal được ràng buộc kỹ lưỡng của bạn. Nó chỉ biết các giá trị và các quy tắc thực tế của ngôn ngữ.
Build thành công không có nghĩa là an toàn khi runtime. Test xanh không có nghĩa là người dùng sẽ thấy một ứng dụng ổn định. Nếu mô hình tư duy của bạn dừng lại ở ranh giới TypeScript, bạn đang "bay trong mù lòa" ngay tại chính nơi các lỗi crash thực sự xảy ra.
Ảo ảnh thời điểm biên dịch
Toàn bộ hệ thống kiểu của TypeScript bị xóa sạch trong quá trình biên dịch. Hãy mở đầu ra JavaScript đã biên dịch của bất kỳ dự án nào, bạn sẽ thấy không còn dấu vết của interface, type, hay các ràng buộc generic. Chúng chỉ là giàn giáo (scaffolding) trong quá trình thiết kế. Trình duyệt hoặc tiến trình Node.js thực thi JavaScript thuần, và các giá trị chảy qua các hàm của bạn không được đảm bảo sẽ khớp với các kiểu mà bạn đã khai báo trên lý thuyết.
Khoảng cách này quan trọng nhất ở các điểm tiếp giáp (edges) của hệ thống. Phản hồi mạng, dữ liệu nhập từ người dùng và các thư viện bên thứ ba có thể chèn vào các giá trị vi phạm kiểu dữ liệu của bạn. Một biến bạn khai báo là strictEmail: string vẫn có thể chứa một con số ở runtime nếu dữ liệu lỗi đi qua một API không đáng tin cậy. TypeScript không thể theo sát mã nguồn của bạn vào production để thực thi bất cứ điều gì. Runtime hoạt động ở một mặt phẳng hoàn toàn tách biệt, và việc đánh đồng hai mặt phẳng này sẽ dẫn đến những lỗi mà phân tích tĩnh sẽ không bao giờ bắt được.
Khi những con số phản bội bạn
TypeScript thấy một number. Engine JavaScript thấy một số thực dấu phẩy động độ chính xác kép IEEE 754. Sự khác biệt đó vô hại cho đến khi nó gây ra thảm họa.
JavaScript cấp phát 64 bit cho mỗi con số, nhưng chỉ có 53 bit dùng để lưu phần mantissa. Điều này tạo ra một giới hạn số nguyên an toàn là 9,007,199,254,740,991. Bất cứ thứ gì lớn hơn sẽ bị làm tròn đến giá trị có thể biểu diễn gần nhất. Trong thực tế, hai định danh thực sự khác nhau có thể bị gộp thành cùng một giá trị bên trong ứng dụng của bạn.
Snowflake IDs và các định danh số nguyên 64-bit phân tán khác thường xuyên vượt quá giới hạn đó. Các hệ thống tài chính theo dõi các khoản tiền lớn ở các đơn vị tiền tệ nhỏ cũng có thể chạm tới giới hạn này. Nguy hiểm thường xuất hiện trước khi logic nghiệp vụ của bạn kịp chạy: JSON.parse sẽ chuyển đổi các numeric literal trong một payload thành số JavaScript một cách vội vã, âm thầm làm mất độ chính xác ngay khi nhận dữ liệu. Định nghĩa kiểu của bạn có thể hứa hẹn id: number, nhưng giá trị runtime đã bị hỏng trước khi hàm đầu tiên được gọi.
Cách khắc phục rất đơn giản nhưng đòi hỏi sự kỷ luật trong toàn bộ stack của bạn. Hãy giữ các định danh lớn dưới dạng string trong quá trình truyền tải qua mạng. Trong các JSON schema và hợp đồng API, hãy định nghĩa các trường này là string, không phải number. Nếu bạn bắt buộc phải thực hiện phép tính trên các giá trị vượt quá phạm vi an toàn, hãy sử dụng BigInt. Tuy nhiên, hãy cẩn thận: BigInt không thể tự động kết hợp với các số JavaScript tiêu chuẩn, và JSON.stringify không thể serialize một BigInt mà không gây lỗi trừ khi bạn chuyển đổi nó về string trước. Hãy coi các ID là các opaque token theo mặc định. Chỉ parse sang dạng số khi bạn đang ở trong một module tính toán biệt lập thực sự cần
