TypeScript satisfies operator là gì? Kiểm tra kiểu không đổi biến

TypeScript satisfies operator (toán tử satisfies) cho phép kiểm tra kiểu của một biểu thức phù hợp với kiểu mong đợi mà không thay đổi kiểu suy luận gốc của biểu thức đó. satisfies ra mắt từ TypeScript 4.9, giải quyết bài toán lâu đời: ép kiểu (as) làm mất thông tin kiểu chi tiết, trong khi annotation kiểu tường minh (const x: Type) lại ép buộc kiểu rộng hơn.

Khi bạn viết const config = { url: "https://api.example.com", timeout: 5000 } satisfies AppConfig, TypeScript kiểm tra config có đầy đủ các trường bắt buộc của AppConfig, nhưng kiểu của config vẫn là { readonly url: "https://api.example.com"; readonly timeout: 5000 } — giữ nguyên literal types thay vì rộng thành stringnumber.

So sánh kiểu suy luận với as vs satisfies vs annotation trong TypeScript

Vấn đề trước satisfies

Giả sử bạn định nghĩa kiểu cấu hình:

interface AppConfig {
  url: string;
  timeout: number;
  retries?: number;
}

Với type annotation:

const config: AppConfig = {
  url: "https://api.example.com",
  timeout: 5000
};
// Kiểu config: AppConfig (url: string, timeout: number)
// Mất literal types, không biết url chính xác là gì

Với type assertion (as):

const config = {
  url: "https://api.example.com",
  timeout: 5000
} as AppConfig;
// Kiểu config: { url: string; timeout: number; }
// Cũng mất literal types, và không kiểm tra dư trường

Cả hai cách đều làm mất thông tin literal type quan trọng cho type-level programming, exhaustiveness checking, và autocompletion chính xác.

Cú pháp và hành vi của satisfies

expression satisfies Type

Quy tắc:

  • Expression phải phù hợp (assignable to) Type — nếu không, TypeScript báo lỗi.
  • Kiểu kết quả của expression giữ nguyên (không widened thành Type).
  • Không tạo ra giá trị runtime mới — satisfies bị xóa hoàn toàn khi compile.

Ví dụ thực tế

1. Giữ literal types cho object config

interface ThemeConfig {
  colors: {
    primary: string;
    secondary: string;
  };
  spacing: number[];
}

const theme = {
  colors: {
    primary: "#0066cc",
    secondary: "#f0f0f0"
  },
  spacing: [4, 8, 16, 24]
} satisfies ThemeConfig;

// Kiểu theme.colors.primary: "#0066cc" (literal)
// Kiểu theme.spacing: readonly [4, 8, 16, 24] (tuple readonly)

Khi dùng theme.colors.primary trong code, TypeScript biết chính xác giá trị là "#0066cc", cho phép so sánh type-level hay dùng làm key object type-safe.

2. Kiểm tra exhaustiveness cho union type

type Status = "loading" | "success" | "error";

const messages = {
  loading: "Đang tải...",
  success: "Thành công!",
  error: "Có lỗi xảy ra"
} satisfies Record;

// Nếu thêm "pending" vào Status nhưng quên cập nhật messages,
// TypeScript báo lỗi: Property 'pending' is missing

Đây là pattern phổ biến để đảm bảo mapping hoàn chỉnh giữa union type và object.

3. Kiểm tra dư trường (excess property checking)

interface User {
  name: string;
  email: string;
}

const user = {
  name: "Nguyễn Văn A",
  email: "[email protected]",
  age: 25  // Lỗi: 'age' does not exist in type 'User'
} satisfies User;

Khác với as User (chỉ widen, không kiểm tra dư), satisfies báo lỗi trường thừa — giúp bắt lỗi chính tả key object.

Minh họa error message khi thừa trường với satisfies

Khi nào dùng satisfies thay vì as hoặc annotation?

Trường hợp Khuyên dùng Lý do
Config object cần literal types satisfies Giữ literal, kiểm tra shape
Mapping union → object satisfies Record Exhaustiveness checking
Callback handler type-safe satisfies Giữ параметр types chính xác
Cần ép kiểu rộng hơn (upcast) annotation (: Type) satisfies không widen
Biết chắc expression đúng kiểu, muốn tắt check as Type satisfies vẫn check

Các cạm bẫy thường gặp

1. satisfies không widen — đôi khi bạn CẦN widen

const values = [1, 2, 3] satisfies number[];
// Kiểu: readonly [1, 2, 3] (tuple readonly)
// Không thể push: values.push(4) // Error

// Cần widen thì dùng annotation:
const values: number[] = [1, 2, 3];
// Kiểu: number[], có thể push

2. satisfies với function expression

type Handler = (x: number) => string;

const handler = (x: number) => x.toString() satisfies Handler;
// OK, nhưng kiểu handler vẫn là (x: number) => string
// Không khác gì annotation trong trường hợp này

3. satisfies không làm deep partial

interface DeepConfig {
  database: {
    host: string;
    port: number;
  };
}

const config = {
  database: {
    host: "localhost"
    // thiếu port
  }
} satisfies DeepConfig;
// Lỗi: Property 'port' is missing in type '{ host: string }'
// satisfies kiểm tra deep structure

Kết hợp với const assertions

as const khiến object thành readonly và literal types toàn bộ. satisfies bổ sung kiểm tra shape:

const rawConfig = {
  api: {
    baseUrl: "https://api.example.com",
    version: "v1"
  },
  features: ["auth", "billing", "analytics"]
} as const;

// Kiểm tra shape mà không mất readonly/literal:
const config = rawConfig satisfies {
  api: { baseUrl: string; version: string };
  features: readonly string[];
};

Hiệu năng compile-time

satisfies không tăng chi phí type-checking đáng kể. Nó thực chất là một type guard ở mức biểu thức, tương tự như kiểm tra assignability trong annotation nhưng bảo toàn kiểu gốc. Trong project lớn, satisfies giúp giảm dùng as không an toàn và annotation quá rộng, cải thiện type inference toàn cục.

Kết luận

TypeScript satisfies operator là công cụ tinh tế cho type safety: nó kiểm tra “đúng hình dạng” mà không ép “đúng kiểu rộng”. Dùng satisfies khi bạn muốn giữ literal types, bắt lỗi trường thừa, đảm bảo exhaustiveness mapping — những tình huống mà as quá lỏng và : Type quá chặt. Đây là minh chứng cho triết lý TypeScript: kiểu tĩnh phục vụ developer, không cản trở suy luận tự nhiên của ngôn ngữ.

Nguồn tham khảo: TypeScript 4.9 Release Notes – satisfies, PR gốc introduces satisfies

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Turborepo: Hệ thống build monorepo siêu nhanh từ Vercel

Turbo hay Turborepo là hệ thống build monorepo mã nguồn mở được Vercel phát triển, viết bằng Rust để đạt tốc độ cao nhất. Trong môi trường JavaScript và TypeScript…

Xem thêm

Homebrew Package Manager quản lý công cụ trên macOS

Homebrew là gì? Homebrew là package manager mã nguồn mở cho macOS và Linux. Nó giúp lập trình viên cài, cập nhật, gỡ cài đặt công cụ dòng lệnh (CLI)…

Xem thêm

Git Worktree: Quản lý nhiều branch song song trong một repo

Tại sao cần Git Worktree? Git workflow thông thường ép bạn chuyển branch liên tục: git checkout feature-A, sửa, rồi git checkout main, hotfix, rồi quay lại. Mỗi lần checkout…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất