Xử lý lỗi với never và Exhaustive Check
Bài 14 – never, exhaustive check và cách tránh lỗi logic trong TypeScript theo cách đơn giản, trực quan và áp dụng ngay.
Xử lý lỗi với never và Exhaustive Check
Kiểu never và kỹ thuật exhaustive check là công cụ quan trọng giúp bạn bắt lỗi logic ngay khi compile. Đây là cách TypeScript buộc bạn xử lý đầy đủ tất cả trường hợp có thể xảy ra, đặc biệt trong union type và state machine.
1. Nội dung chính
- Kiểu never là gì
- Khi nào hàm trả về never
- Exhaustive check là gì và vì sao quan trọng
- Ứng dụng exhaustive check trong xử lý trạng thái
- Tránh lỗi runtime nhờ kiểm soát toàn bộ case
2. Ví dụ chi tiết
2.1. Kiểu never là gì?
// Kiểu never đại diện cho "không có giá trị nào"
// Nó xuất hiện khi TypeScript biết rằng một giá trị sẽ không bao giờ xảy ra
type NeverType = string & number; // never (không có giá trị nào vừa là string vừa là number)
// never trong conditional types
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
2.2. Hàm trả về never
// 1. Hàm luôn throw error
function fail(message: string): never {
throw new Error(message);
}
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
fail(message); // Hàm này trả về never
}
}
// Usage
function divide(a: number, b: number): number {
assert(b !== 0, "Cannot divide by zero");
return a / b; // TypeScript biết b !== 0 ở đây
}
// 2. Hàm với infinite loop
function infiniteLoop(): never {
while (true) {
console.log("Running forever...");
}
}
// 3. Hàm gọi process.exit()
function exitProcess(code: number): never {
process.exit(code);
}
2.3. Exhaustive check với union type
// Exhaustive check đảm bảo bạn xử lý tất cả các trường hợp
type Status = "success" | "error" | "loading" | "idle";
function handleStatus(status: Status): string {
switch (status) {
case "success":
return "✅ Operation completed successfully!";
case "error":
return "❌ An error occurred!";
case "loading":
return "⏳ Loading...";
case "idle":
return "💤 Waiting for action...";
default:
// Exhaustive check - đảm bảo tất cả cases đã được xử lý
const _exhaustive: never = status;
return _exhaustive;
}
}
// Nếu bạn thêm một giá trị mới vào Status mà không xử lý nó,
// TypeScript sẽ báo lỗi ở dòng const _exhaustive: never = status;
2.4. Exhaustive check với discriminated unions
// Discriminated unions - pattern phổ biến cho state management
type ServerState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User[] }
| { status: "error"; error: string };
interface User {
id: number;
name: string;
email: string;
}
function handleServerState(state: ServerState): string {
switch (state.status) {
case "idle":
return "Ready to fetch data";
case "loading":
return "Fetching data...";
case "success":
return `Found ${state.data.length} users`;
case "error":
return `Error: ${state.error}`;
default:
// Exhaustive check cho discriminated union
const _exhaustive: never = state;
return _exhaustive;
}
}
2.5. Exhaustive check trong reducer
// Action types cho counter reducer
type CounterAction =
| { type: "INCREMENT"; amount: number }
| { type: "DECREMENT"; amount: number }
| { type: "RESET" }
| { type: "MULTIPLY"; factor: number };
interface CounterState {
count: number;
history: number[];
}
function counterReducer(
state: CounterState,
action: CounterAction
): CounterState {
switch (action.type) {
case "INCREMENT":
return {
...state,
count: state.count + action.amount,
history: [...state.history, state.count + action.amount]
};
case "DECREMENT":
return {
...state,
count: state.count - action.amount,
history: [...state.history, state.count - action.amount]
};
case "RESET":
return {
count: 0,
history: [0]
};
case "MULTIPLY":
return {
...state,
count: state.count * action.factor,
history: [...state.history, state.count * action.factor]
};
default:
// Exhaustive check - nếu thêm action mới mà không xử lý, sẽ báo lỗi
const _check: never = action;
return _check;
}
}
2.6. Generic exhaustive check helper
// Tạo utility function cho exhaustive check
function exhaustiveCheck(value: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}
// Sử dụng trong switch statements
function processPayment(method: PaymentMethod): string {
switch (method.type) {
case "credit-card":
return `Processing credit card payment: $${method.amount}`;
case "paypal":
return `Processing PayPal payment: $${method.amount}`;
case "bank-transfer":
return `Processing bank transfer: $${method.amount}`;
default:
return exhaustiveCheck(method); // TypeScript sẽ báo lỗi nếu thêm method mới
}
}
type PaymentMethod =
| { type: "credit-card"; amount: number; cardNumber: string }
| { type: "paypal"; amount: number; email: string }
| { type: "bank-transfer"; amount: number; accountNumber: string };
2.7. Exhaustive check với if-else chains
// Exhaustive check với if-else thay vì switch
type Theme = "light" | "dark" | "system";
function getThemeColors(theme: Theme): { primary: string; background: string } {
if (theme === "light") {
return { primary: "#007bff", background: "#ffffff" };
} else if (theme === "dark") {
return { primary: "#17a2b8", background: "#343a40" };
} else if (theme === "system") {
return { primary: "auto", background: "auto" };
} else {
// Exhaustive check cho if-else chain
const _exhaustive: never = theme;
return _exhaustive;
}
}
3. Kiến thức trọng tâm
3.1. Kiểu never xuất hiện trong các tình huống đặc biệt
- Hàm không bao giờ return (throw error, infinite loop, process.exit)
- Type operations không hợp lệ (string & number)
- Unreachable code paths trong conditional types
3.2. Exhaustive check đảm bảo xử lý tất cả các trường hợp
- Bắt lỗi compile-time khi thêm cases mới
- Tránh lỗi runtime do unhandled cases
- Đặc biệt quan trọng với Redux, Vuex, Zustand
3.3. Kỹ thuật này cực mạnh trong hệ thống nhiều trạng thái
- State management - Redux, Vuex, Zustand
- API response handling - success, error, loading states
- UI state machines - form validation, multi-step workflows
💡 GHI NHỚ: Exhaustive check là “bảo hiểm” cho codebase của bạn
4. Bài tập thực hành
Bài 1: Tạo hàm panic và assert
// 1. Tạo hàm panic luôn throw error
function panic(message: string): never {
throw new Error(`[PANIC] ${message}`);
}
// 2. Tạo hàm assert với type guards
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
panic(message);
}
}
// 3. Sử dụng trong functions
function safeDivide(a: number, b: number): number {
assert(b !== 0, "Division by zero is not allowed");
assert(typeof a === "number" && typeof b === "number", "Both arguments must be numbers");
return a / b;
}
Bài 2: Shape calculator với exhaustive check
// Tạo union type cho các hình học
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
// Tính diện tích với exhaustive check
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// Exhaustive check
const _exhaustive: never = shape;
return _exhaustive;
}
}
// Tính chu vi với exhaustive check
function calculatePerimeter(shape: Shape): number {
switch (shape.kind) {
case "circle":
return 2 * Math.PI * shape.radius;
case "square":
return 4 * shape.side;
case "rectangle":
return 2 * (shape.width + shape.height);
case "triangle":
// Giả sử là tam giác vuông
const hypotenuse = Math.sqrt(shape.base ** 2 + shape.height ** 2);
return shape.base + shape.height + hypotenuse;
default:
const _exhaustive: never = shape;
return _exhaustive;
}
}
Bài 3: Todo app reducer với exhaustive check
// Action types cho todo app
type TodoAction =
| { type: "ADD_TODO"; text: string }
| { type: "TOGGLE_TODO"; id: number }
| { type: "DELETE_TODO"; id: number }
| { type: "CLEAR_COMPLETED" }
| { type: "SET_FILTER"; filter: "all" | "active" | "completed" };
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
filter: "all" | "active" | "completed";
nextId: number;
}
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [...state.todos, {
id: state.nextId,
text: action.text,
completed: false
}],
nextId: state.nextId + 1
};
case "TOGGLE_TODO":
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case "DELETE_TODO":
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.id)
};
case "CLEAR_COMPLETED":
return {
...state,
todos: state.todos.filter(todo => !todo.completed)
};
case "SET_FILTER":
return {
...state,
filter: action.filter
};
default:
// Exhaustive check - đảm bảo tất cả actions được xử lý
const _check: never = action;
return _check;
}
}
5. Sai lầm thường gặp
- Quên exhaustive check - dẫn đến unhandled cases
- Dùng never cho functions có thể return - làm mất type safety
- Không document exhaustive check patterns - teammates không hiểu
- Mix exhaustive check với regular default cases - inconsistent patterns
- Không handle edge cases - missing validation cho input types
⚠️ GHI NHỚ: Exhaustive check là investment cho maintainable code
6. Kết luận
Kiểu never và exhaustive check giúp bạn xây dựng hệ thống logic an toàn, tránh lỗi khó đoán và đảm bảo mọi trường hợp đều được xử lý trước khi chạy. Đây là kỹ thuật cực kỳ quan trọng cho bất kỳ ứng dụng TypeScript nào có nhiều states hoặc complex logic.
🔑 GHI NHỚ QUAN TRỌNG:
- never cho unreachable code và non-returning functions
- Exhaustive check cho complete case handling
- Luôn sử dụng exhaustive check với union types
- Document exhaustive check patterns cho team
- Đây là “bảo hiểm” compile-time cho logic của bạn