•10 min read

Cách TypeScript kiểm tra toàn diện loại bỏ toàn bộ các loại lỗi

Cách TypeScript kiểm tra toàn diện loại bỏ toàn bộ các loại lỗi
Type Safety

never type

Click to reveal
Type Safety
Kiểu dữ liệu đáy của TypeScript — kiểu không có giá trị khả dĩ nào. Được dùng làm kiểu trả về cho các hàm không bao giờ trả về bình thường (luôn ném lỗi hoặc lặp vô hạn).

never type

Type Safety

exhaustive check

Click to reveal
Type Safety
Một mẫu sử dụng `switch` với trường hợp mặc định có kiểu `never`. TypeScript sẽ báo lỗi khi biên dịch nếu bạn thêm một thành viên mới vào union nhưng quên xử lý nó trong bất kỳ switch nào.

exhaustive check

Type Safety

type guard

Click to reveal
Type Safety
Một hàm vị từ hoặc kiểm tra tại thời điểm chạy giúp thu hẹp một kiểu union thành một trong các thành viên của nó. Được sử dụng với toán tử `in`, `typeof`, hoặc các vị từ tùy chỉnh với từ khóa `is`.

type guard

Type Safety

discriminated union

Click to reveal
Type Safety
Một union mà mọi thành viên đều chia sẻ một thuộc tính có kiểu literal chung (cái 'tag'). TypeScript thu hẹp kiểu dựa trên thuộc tính đó mà không cần các guard rõ ràng.

discriminated union

Cách Kiểm Tra Toàn Diện của TypeScript Loại Bỏ Toàn Bộ Các Loại Lỗi

TypeScript bắt được rất nhiều lỗi ngay tại thời điểm biên dịch. Nhưng có một mẫu hình biến nó từ một trợ lý hữu ích thành một kiểm toán viên không ngừng nghỉ: kiểm tra toàn diện với kiểu never. Sự khác biệt là một trường hợp bị thiếu trong switch không tạo ra cảnh báo mơ hồ "cái này có thể là undefined" — nó tạo ra một lỗi cứng ngay tại dòng bạn đã quên.

Audio Briefing
0:00 / 0:00

Vấn Đề Nó Giải Quyết

Mỗi câu lệnh switch giống enum là một quả bom hẹn giờ trong một codebase đang phát triển. Bạn viết một trình xử lý cho ba trường hợp. Ba tháng sau, ai đó thêm một biến thể thứ tư. Nó biên dịch. Các bài kiểm tra vượt qua. Sản phẩm âm thầm bỏ qua trường hợp mới. Không có lỗi cho đến khi khách hàng báo cáo.

Kiểm tra toàn diện là giải pháp. Nó biến trình biên dịch thành người đánh giá của bạn, chứ không chỉ là công cụ kiểm tra chính tả.

Advertisement

Kiểu never Trong Thực Tế

Kiểu never của TypeScript đại diện cho các giá trị không bao giờ xảy ra. Một hàm luôn ném lỗi sẽ trả về never. Mẫu kiểm tra toàn diện khai thác điều đó: khi TypeScript nghĩ một nhánh có kiểu never, nhưng bạn đang cố gắng gán kết quả của nó cho một thứ khác, nó sẽ báo lỗi — mọi lúc.

Đây là mẫu cốt lõi:

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

function renderStatus(status: Status): string {
  switch (status) {
    case "idle":
      return "Waiting...";
    case "loading":
      return "Loading...";
    case "success":
      return "Done.";
    case "error":
      return "Something went wrong.";
    default:
      // The exhaustiveness check
      const _exhaustive: never = status;
      return _exhaustive;
  }
} ```

The assignment `const _exhaustive: never = status` is unreachable by design. If you add a new `Status` member and forget to add a `case` above, TypeScript assigns `"new-status"` to `_exhaustive` and immediately reports:

Type '"new-status"' is not assignable to type 'never'.


It does this at compile time. No tests needed for this particular bug category.

## A Real-World Example

<Callout type="code" title="API Response Handler">
```typescript
type ApiResponse<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: ApiError };

function showResponse<T>(response: ApiResponse<T>) {
  switch (response.status) {
    case "idle":
      return <EmptyState />;
    case "loading":
      return <Spinner />;
    case "success":
      return <DataView data={response.data} />;
    case "error":
      return <ErrorBanner message={response.error.message} />;
    default:
      const _exhaustive: never = response;
      return _exhaustive;
  }
} ```

</Callout>

Mỗi nhánh thu hẹp discriminated union một cách chính xác. Nếu sau này ai đó thêm `"retry"` vào `ApiResponse` nhưng quên thêm một nhánh `case "retry"`, trường hợp `default` sẽ bị lỗi ngay tại dòng đó. Không có bất ngờ nào khi chạy.

## Hàm Trợ Giúp

Nhiều codebase thích một hàm có tên hơn là khai báo biến nội tuyến. Nó dễ đọc và cung cấp một nơi duy nhất để kiểm tra khi có gì đó không ổn:

```typescript
function assertNever(
  x: never,
  message = "Unexpected exhaustive case"
): never {
  throw new Error(`${message}: ${JSON.stringify(x)}`);
} ```

Usage:

```typescript
function processState(state: AppState) {
  switch (state.type) {
    case "init": return init(state);
    case "fetch": return fetchData(state);
    case "render": return render(state);
    default: return assertNever(state, "Unhandled AppState");
  }
} ```

Kiểu trả về của hàm là `never`, vì vậy TypeScript từ chối biên dịch lệnh gọi nếu `state` thực sự không phải là `never`. Sự từ chối đó là tín hiệu lỗi.

## Tại Sao Không Chỉ Là Một Default Ném Lỗi?

Một cách tiếp cận ngây thơ là ném lỗi từ `default` mà không cần kiểm tra `never`:

```typescript
default:
  throw new Error(`Unhandled status: ${status}`);

Điều này có vẻ tương tự nhưng có một lỗ hổng nghiêm trọng: nó biên dịch âm thầm với bất kỳ giá trị Status mới nào. Lỗi thời gian chạy chỉ xảy ra khi đường dẫn đó thực sự được truy cập trong sản xuất. Kiểm tra toàn diện xảy ra tại thời điểm biên dịch, trước khi bất kỳ ai triển khai bất cứ điều gì.

Khía cạnhDefault ném lỗi ngây thơKiểm tra never toàn diện
Thêm một trường hợp mới
Biên dịch âm thầm
Thất bại tại thời điểm biên dịch
Phát hiện lỗi
Xuất hiện tại thời điểm chạy trong sản phẩm
Xuất hiện trong trình chỉnh sửa, trước khi commit
Tiến hóa kiểu
Dễ quên
Được hệ thống kiểu thực thi
Hỗ trợ IDE
Không có tín hiệu
Đánh dấu dòng chính xác

Sử Dụng Với Discriminated Unions

Kiểm tra toàn diện tỏa sáng nhất với các discriminated union lớn. Máy trạng thái, các nút AST và các payload sự kiện là những mục tiêu phổ biến:

type AuthEvent =
  | { kind: "login"; username: string }
  | { kind: "logout" }
  | { kind: "token_refresh"; token: string }
  | { kind: "session_expired"; reason: string };

function logAuthEvent(event: AuthEvent) {
  switch (event.kind) {
    case "login":
      console.log(`${event.username} logged in`);
      break;
    case "logout":
      console.log("User logged out");
      break;
    case "token_refresh":
      console.log(`Token refreshed, ends ${event.token.slice(-4)}`);
      break;
    case "session_expired":
      console.log(`Session expired: ${event.reason}`);
      break;
    default:
      const _exhaustive: never = event;
      return _exhaustive;
  }
} ```

Adding `{ kind: "mfa_challenge" }` without a corresponding `case` breaks compilation immediately. This is the behavior you want in security-sensitive code where silently ignoring a case can mean silently bypassing a check.

## Exhaustiveness in `if/else` Chains

```typescript
function getRoleName(role: UserRole): string {
  if (role === "admin") return "Administrator";
  if (role === "editor") return "Content Editor";
  if (role === "viewer") return "Read-only Viewer";
  // Exhaustiveness check at the end
  const _exhaustive: never = role;
  return _exhaustive;
} ```

Mẫu này hoạt động giống hệt trong các chuỗi `if/else`. Nó đọc một cách tự nhiên ở cuối chuỗi như một dấu hiệu "không có gì khác nên đến đây".

## Khả Năng Tương Thích Công Cụ

- TypeScript 5.x (tất cả các phiên bản hiện tại) — kiểm tra gán `never` đã ổn định từ các phiên bản đầu. - Chế độ nghiêm ngặt được khuyến nghị: kiểm tra toàn diện dựa vào trình biên dịch để đảm bảo rằng một nhánh không thể truy cập thực sự là `never`. Nó hoạt động mà không cần chế độ nghiêm ngặt, nhưng tín hiệu yếu hơn trong chế độ `noImplicitAny`-off. - ESLint: quy tắc `@typescript-eslint/switch-exhaustiveness-check` bao gồm hầu hết các trường hợp discriminated-union mà không cần biến `never` thủ công. Ưu tiên quy tắc ESLint khi có sẵn, giữ mẫu thủ công cho các công cụ cũ hơn. - IDEs: VSCode, WebStorm và Zed đều hiển thị sự không khớp nội tuyến tại vị trí gán — không cần bước build.

## Hạn Chế và Các Trường Hợp Đặc Biệt

- Thủ thuật gán `never` không hoạt động trực tiếp bên trong các callback `Map` hoặc biểu thức ternary — hãy ưu tiên một hàm trợ giúp có tên trong các ngữ cảnh đó. - Khi một `switch` bao gồm một union bên trong một mapped type, kiểm tra toàn diện có thể tạo ra các kết quả dương tính giả khi truy cập thuộc tính. Chia kiểm tra thành một hàm riêng biệt để giữ cho thông báo lỗi dễ đọc. - Kiểm tra toàn diện thêm một chi phí nhỏ khi biên dịch đối với các union rất lớn (hơn 100 biến thể). Chỉ đáng kể trong các monorepo có nhiều câu lệnh `switch` như vậy. - Không thay thế các bài kiểm tra cho *lỗi logic* trong một trường hợp — nó chỉ bắt được loại *trường hợp bị thiếu*.

## Nơi Áp Dụng Mẫu Này

Những nơi có hiệu quả cao nhất để kiểm tra toàn diện trong một codebase điển hình:

- **Máy trạng thái** (trạng thái UI, trạng thái xác thực, trạng thái thanh toán) — mọi trạng thái mới phải được xử lý rõ ràng. - **Các trình duyệt AST hoặc trình xử lý sự kiện** — việc thêm một loại nút hoặc sự kiện mới phải tạo ra một nhánh mới hoặc một lỗi lớn. - **Các kiểu union phản hồi API** — các biến thể phản hồi mới phải hiển thị một đường dẫn render mới. - **Các bộ điều phối lệnh con CLI** — việc thêm một lệnh con mới phải thêm một trình xử lý hoặc không biên dịch được.

## Tham Khảo Nhanh

```typescript
// Phiên bản nội tuyến (phổ biến nhất)
function handleStatus(status: Status): string {
  switch (status) {
    case "idle": return "Waiting";
    case "loading": return "Loading...";
    case "success": return "Done";
    case "error": return "Failed";
    default: { const _exhaustive: never = status; return _exhaustive; } }
}

// Phiên bản hàm trợ giúp (sạch hơn cho các switch lớn)
function assertNever(x: never, msg?: string): never {
  throw new Error(msg ?? `Unhandled case: ${JSON.stringify(x)}`);
}

// Với discriminated union
type Tree =
  | { type: "leaf"; color: string }
  | { type: "branch"; children: Tree[] }
  | { type: "fruit"; kind: string };

function describeTree(tree: Tree): string {
  switch (tree.type) {
    case "leaf": return `Leaf: ${tree.color}`;
    case "branch":
      return `Branch with ${tree.children.length} children`;
    case "fruit": return `Fruit: ${tree.kind}`;
    default: return assertNever(tree);
  }
} ```

## Bạn Cũng Có Thể Thích

- [JavaScript to Luau: The Complete Roblox Scripting Cheat Sheet (2026)](/en/blog/javascript-to-luau-roblox-scripting)
- [const vs Immutability in JavaScript: The Binding, Not the Value](/en/blog/const-in-js)
- [How to Get the Correct Y Position of an Element in ChatGPT UI (2026 Guide)](/en/blog/get_position_of_an_element_in_js)
- [Stop Using Promise.race for Fallbacks: The Case for Promise.any and AggregateError](/en/blog/promise-any-aggregate-error)
Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement