NguyenHN
← Về trang chủ
Frontend

5 tính năng TypeScript hay dùng nhưng dễ hiểu sai

·5 phút đọc
Chia sẻ:

TypeScript phổ biến đến mức nhiều người dùng thành thạo cú pháp cơ bản nhưng vẫn hiểu sai một số cơ chế quan trọng — dẫn tới code vẫn compile được nhưng không thực sự an toàn kiểu dữ liệu như tưởng. Dưới đây là 5 điểm hay gặp nhất.

1. any vs unknown — không phải lúc nào cũng thay thế được nhau

any tắt hoàn toàn type checking cho biến đó — bạn có thể gọi bất kỳ method nào, truy cập bất kỳ property nào mà TypeScript không báo lỗi, kể cả khi sai. unknown thì ngược lại: nhận mọi giá trị (giống any) nhưng KHÔNG cho phép thao tác gì trên nó cho tới khi bạn narrow type (kiểm tra typeof, instanceof, hoặc type guard).

function handleError(error: unknown) {
  console.log(error.message); // ❌ Lỗi: 'error' is of type 'unknown'

  if (error instanceof Error) {
    console.log(error.message); // ✅ Đã narrow, an toàn
  }
}

Quy tắc thực tế: dùng unknown cho dữ liệu không rõ nguồn gốc (response API, catch error, JSON.parse) — bắt buộc phải kiểm tra type trước khi dùng. any chỉ nên dùng khi thực sự không còn cách nào khác (ví dụ tương tác với thư viện JS cũ không có type definition).

2. type vs interface — khác nhau ở khả năng mở rộng

Khác biệt hay bị bỏ qua: interface có thể merge (declaration merging) — khai báo cùng tên interface nhiều lần sẽ tự động gộp lại; type alias thì không, khai báo trùng tên sẽ báo lỗi. Đây là lý do các thư viện public API (như khi bạn cần cho phép user mở rộng type) thường dùng interface.

interface User {
  name: string;
}
interface User {
  age: number;
}
// User bây giờ có cả name và age — declaration merging

type Product = { name: string };
type Product = { price: number }; // ❌ Lỗi: Duplicate identifier 'Product'

Ngược lại, type alias linh hoạt hơn ở chỗ có thể biểu diễn union, intersection, mapped type, conditional type — những thứ interface không làm được trực tiếp. Quy ước phổ biến: dùng interface cho object/class shape có thể mở rộng, dùng type cho union/intersection/utility type phức tạp.

3. Optional chaining (?.) không thay thế được kiểm tra null có chủ đích

?. giúp tránh crash khi truy cập property của null/undefined, nhưng nó im lặng trả về undefined — dễ che giấu bug thực sự (dữ liệu đáng ra không nên null lại bị null mà không ai biết). Non-null assertion (!) thì ngược lại — khẳng định với compiler rằng giá trị chắc chắn không null, nhưng nếu sai thực tế sẽ crash lúc runtime mà TypeScript không cảnh báo được.

// Nguy hiểm: nếu user thực sự null, dòng dưới im lặng trả undefined,
// lỗi sẽ xuất hiện ở chỗ khác xa nơi gây ra, khó debug
const name = user?.profile?.name;

// Cũng nguy hiểm theo cách khác: nếu sai, crash thẳng lúc runtime
const name2 = user!.profile!.name;

// An toàn hơn: xử lý rõ ràng trường hợp null
if (!user) throw new Error("User không tồn tại");
const name3 = user.profile?.name ?? "Ẩn danh";

4. Discriminated union — cách làm state machine an toàn kiểu

Thay vì dùng nhiều field optional/boolean rời rạc để biểu diễn trạng thái (dễ tạo ra tổ hợp vô nghĩa như isLoading: true và data: {...} cùng lúc), discriminated union dùng 1 field chung (thường gọi là status hoặc type) để TypeScript tự suy luận field nào tồn tại trong từng nhánh.

type FetchState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string };

function render(state: FetchState<User>) {
  switch (state.status) {
    case "success":
      return state.data.name; // TS biết chắc data tồn tại ở nhánh này
    case "error":
      return state.error; // TS biết chắc error tồn tại ở nhánh này
  }
}

Lợi ích thực tế: không thể vô tình truy cập state.data khi status là "error" — TypeScript sẽ báo lỗi ngay tại chỗ, thay vì để runtime crash vì data undefined.

5. satisfies — kiểm tra type mà không mất đi type cụ thể

Trước khi có satisfies (TypeScript 4.9+), để vừa kiểm tra 1 object khớp với type mong muốn vừa giữ lại type cụ thể của từng property, người ta phải chọn 1 trong 2: khai báo kiểu tường minh (mất type literal cụ thể) hoặc không khai báo gì (mất kiểm tra). satisfies giải quyết cả hai cùng lúc.

type Config = Record<string, "light" | "dark">;

// Cách cũ: khai báo kiểu tường minh — mất type literal cụ thể
const themeA: Config = { home: "light", admin: "dark" };
themeA.home; // kiểu "light" | "dark", không còn biết chắc là "light"

// Dùng satisfies: vẫn kiểm tra đúng Config, nhưng giữ type cụ thể
const themeB = { home: "light", admin: "dark" } satisfies Config;
themeB.home; // kiểu chính xác là "light"

satisfies đặc biệt hữu ích khi định nghĩa config object (route map, theme map, permission map) — vừa được TypeScript kiểm tra đúng cấu trúc, vừa giữ nguyên autocomplete chính xác cho từng giá trị cụ thể thay vì bị union type rộng hơn mức cần thiết.