DEV Community

Type System — Basic Types

TypeScript basic types: union, literal, và vì sao unknown an toàn hơn any

TypeScript thêm một tầng kiểm tra kiểu lúc biên dịch lên trên JavaScript, bắt cả lớp lỗi "đọc property của undefined", "truyền sai kiểu đối số" trước khi code chạy. Nhưng giá trị đó phụ thuộc vào việc dùng kiểu cho đúng: rải any khắp nơi vô hiệu hóa toàn bộ lợi ích, biến TypeScript thành JavaScript có cú pháp thừa. Hiểu primitive, literal, union, và đặc biệt sự khác nhau giữa anyunknown là nền để type system thật sự bảo vệ thay vì chỉ trang trí.

Cơ chế hoạt động

Kiểu trong TS chỉ tồn tại lúc biên dịch — chúng bị xóa hoàn toàn khi compile sang JavaScript (type erasure), không có kiểm tra kiểu lúc runtime. Các khối cơ bản:

type Status = 'pending' | 'shipped' | 'cancelled' // union of literal types
type Id = string | number                          // union
let x: unknown                                      // chưa biết kiểu, phải kiểm tra trước khi dùng
Enter fullscreen mode Exit fullscreen mode

Literal type ('pending') thu hẹp giá trị xuống đúng một hằng; union (A | B) cho phép một trong nhiều kiểu. Kết hợp chúng tạo nên các kiểu mô tả chính xác miền giá trị hợp lệ, để compiler bắt được khi gán nhầm:

function setStatus(s: Status) { /* ... */ }
setStatus('shiped') // lỗi compile: 'shiped' không thuộc union (bắt được typo)
Enter fullscreen mode Exit fullscreen mode

Vấn đề gặp trong production

Failure mode: any lan truyền và vô hiệu hóa kiểm tra. any nghĩa là "tắt kiểm tra kiểu cho giá trị này" — và nó lây: gán một any cho biến khác làm biến đó cũng mất kiểm tra. Một any ở ranh giới dữ liệu (response API, JSON.parse) có thể làm cả luồng phía sau mất an toàn:

const data: any = await res.json()
const total = data.order.amount * data.qty // không lỗi compile, nhưng runtime nổ nếu cấu trúc khác
Enter fullscreen mode Exit fullscreen mode

Compiler không cảnh báo gì vì any chấp nhận mọi thao tác. Runtime mới phát hiện data.orderundefined — đúng loại lỗi TypeScript đáng lẽ ngăn được. unknown là thay thế an toàn: nó cũng nhận mọi giá trị, nhưng bắt buộc kiểm tra/thu hẹp kiểu trước khi dùng:

const data: unknown = await res.json()
// data.order  // lỗi compile: phải kiểm tra kiểu trước
if (isOrderResponse(data)) {        // type guard thu hẹp về kiểu đã biết
  const total = data.order.amount * data.qty // giờ mới an toàn
}
Enter fullscreen mode Exit fullscreen mode

Failure mode: tin vào kiểu ở ranh giới ngoài. Vì kiểu bị xóa lúc runtime, khai báo const data: OrderResponse = await res.json() không kiểm tra gì lúc chạy — nó chỉ là lời hứa với compiler. Nếu API trả khác, không có lỗi tại điểm nhận, lỗi nổ muộn ở nơi dùng. Dữ liệu từ ngoài (API, form, file, env) phải được validate lúc runtime bằng một schema validator (kiểu như zod) rồi mới gán kiểu, thay vì ép kiểu mù.

Cách debug và monitor

Bật strict trong tsconfig — nó kích hoạt strictNullChecks và loạt kiểm tra bắt phần lớn lỗi null/undefined lúc compile. Khi điều tra một runtime error mà "lẽ ra TypeScript phải bắt", lần theo nguồn của kiểu: gần như luôn có một any (thường ở ranh giới dữ liệu ngoài) hoặc một ép kiểu (as) che mất thực tế. Bật lint rule no-explicit-any để mỗi any phải cố ý và lộ ra trong review. Đếm số any/as trong codebase như một chỉ số nợ kỹ thuật về type-safety; tập trung thay chúng ở các ranh giới (API client, parse) trước vì đó là nơi lỗi lan xa nhất.

Tradeoff

Strict typing tăng an toàn — bắt lỗi lúc compile, làm tài liệu sống cho code, giúp refactor an toàn — đổi lại tốn công viết kiểu và đôi khi phải "thuyết phục" compiler ở chỗ logic phức tạp. any cho lối thoát nhanh khi gấp nhưng phá vỡ chính lý do dùng TypeScript; unknown giữ được sự linh hoạt "chưa biết kiểu" mà không bỏ an toàn, đổi lại buộc phải thu hẹp kiểu trước khi dùng. Quy tắc thực tế: strict mặc định; không any (dùng unknown + type guard hoặc kiểu cụ thể); validate runtime ở mọi ranh giới dữ liệu ngoài vì kiểu bị xóa và không tự kiểm tra lúc chạy.

Câu hỏi phỏng vấn

unknown khác any thế nào, và vì sao gán kiểu cho await res.json() không đảm bảo dữ liệu đúng kiểu lúc runtime?

any tắt hoàn toàn kiểm tra kiểu cho giá trị đó và lây sang nơi nó được gán, cho phép mọi thao tác mà không cảnh báo — nên một any ở ranh giới dữ liệu làm cả luồng phía sau mất an toàn, lỗi chỉ nổ lúc runtime. unknown cũng nhận mọi giá trị nhưng bắt buộc thu hẹp/kiểm tra kiểu (type guard) trước khi dùng, nên giữ được sự linh hoạt mà không bỏ an toàn. Gán kiểu cho res.json() không đảm bảo gì lúc runtime vì kiểu trong TS bị xóa hoàn toàn khi compile (type erasure) — const data: OrderResponse = await res.json() chỉ là lời hứa với compiler, không có kiểm tra lúc chạy; nếu API trả khác thì lỗi nổ muộn ở nơi dùng. Điểm ăn điểm: validate dữ liệu ngoài bằng schema validator (zod...) lúc runtime rồi mới gán kiểu, bật strict, và cấm any qua lint.

Hands-on

Lấy một API client thật nhận response phức tạp, đầu tiên gán any cho kết quả res.json() và viết logic dùng các field lồng sâu; cố tình cho server trả về cấu trúc khác và quan sát không có lỗi compile nhưng runtime nổ ở nơi dùng. Đổi any thành unknown và để compiler buộc viết một type guard (hoặc dùng zod parse) trước khi truy cập, xác nhận lỗi cấu trúc giờ bị bắt ngay tại ranh giới với thông báo rõ. Bật strictno-explicit-any trong cấu hình, rà codebase đếm số any/as còn lại, và thay dần các chỗ ở ranh giới dữ liệu ngoài.

Top comments (0)