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

Table of Contents
never type
never type
exhaustive check
exhaustive check
type guard
type guard
discriminated union
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.
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ả.
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ạnh | Default 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)
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

gRPC vs ConnectRPC: Microservices hiện đại và Protobuf gốc trình duyệt
Đánh giá kiến trúc gRPC vs ConnectRPC trong TypeScript và Go, khám phá streaming HTTP/1.1 vs HTTP/2, client trình duyệt không cần proxy Envoy và độ trễ RPC p99.
Read more
Các Mẫu TypeScript Nâng Cao cho Ứng Dụng Doanh Nghiệp
Nắm vững các mẫu TypeScript doanh nghiệp nâng cao: branded types, conditional response types, template literal routing và toán tử satisfies.
Read more
TypeScript thôi là chưa đủ: Đảm bảo an toàn kiểu dữ liệu end-to-end với Zod trong Next.js
TypeScript biến mất khi runtime. Tìm hiểu lý do tại sao các kiểu tĩnh thất bại ở ranh giới API và Server Action của bạn, và cách Zod mang lại xác thực schema không thể sai sót và suy luận kiểu.
Read more