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