Vấn đề
Bạn đang cố gắng ghi lại một thông báo lỗi trong khối try-catch tiêu chuẩn, nhưng TypeScript chặn đứng bạn bằng một đường kẻ nguệch ngoạc màu đỏ. Nó trông giống như thế này:
try {
// risky logic here
} catch (error) {
console.error(error.message); // Error: Object is of type 'unknown'. (TS18046)
}
Điều này xảy ra vì TypeScript hiện đại (bắt đầu từ phiên bản 4.0 và được thực thi nghiêm ngặt hơn trong 4.4) đặt mặc định các biến trong catch là unknown thay vì any. Đây là một biện pháp bảo vệ. Trong JavaScript, một câu lệnh throw có thể trả về bất cứ thứ gì—một chuỗi, một số, hoặc thậm chí là một đối tượng literal như { code: 500 }. Vì compiler không thể đảm bảo rằng lỗi đó là một đối tượng Error thực sự, nó ngăn bạn truy cập .message để tránh ứng dụng bị crash khi runtime.
Cách khắc phục lỗi TS18046
1. Cách chuẩn nhất: 'instanceof' Type Guard
Cách gọn gàng nhất để làm hài lòng compiler là xác minh danh tính của lỗi. Bằng cách kiểm tra xem đối tượng có phải là một instance của class Error có sẵn hay không, TypeScript sẽ tự động thu hẹp kiểu dữ liệu (narrow type), cho phép bạn truy cập message và stack một cách an toàn.
try {
// Thực hiện một thao tác
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error("An unexpected non-error object was thrown", error);
}
}
Phương pháp này xử lý được những trường hợp hiếm gặp khi một thư viện có thể throw một chuỗi thuần túy thay vì một đối tượng Error chuẩn chỉnh.
2. Cách nhanh nhất: Type Assertion
Đôi khi bạn đang làm việc trên một bản mẫu (prototype) nhanh hoặc một script nội bộ nơi bạn biết chính xác những gì được throw ra. Trong những trường hợp này, bạn có thể sử dụng một type assertion để bỏ qua các bước rườm rà. Chỉ cần cẩn thận: nếu lỗi không giống như bạn mong đợi, err.message sẽ chỉ trả về undefined khi runtime mà không có cảnh báo nào.
try {
// logic xử lý
} catch (error) {
const err = error as Error;
console.log(err.message);
}
3. Xử lý lỗi Axios và API
Các API client như Axios cung cấp các công cụ thu hẹp kiểu dữ liệu của riêng chúng. Nếu bạn đang fetch dữ liệu, lỗi có thể chứa một đối tượng response với mã trạng thái (như 404 hoặc 500). Hãy sử dụng guard có sẵn của thư viện để đạt kết quả tốt nhất.
import axios from 'axios';
try {
await axios.get('/api/users/1');
} catch (error) {
if (axios.isAxiosError(error)) {
// Truy cập các thuộc tính Axios cụ thể một cách an toàn
console.error(`Status: ${error.response?.status} - ${error.message}`);
} else {
console.error("Native error:", error);
}
}
4. Cách sạch nhất: Sử dụng Helper có thể tái sử dụng
Việc viết if (error instanceof Error) trong mọi khối catch sẽ nhanh chóng trở nên lặp đi lặp lại. Tôi khuyên bạn nên chuyển logic này vào một hàm tiện ích (utility function). Điều này giúp các khối catch của bạn gọn nhẹ và đảm bảo việc ghi log lỗi nhất quán trong toàn bộ mã nguồn.
function ensureErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
return "An unknown error occurred";
}
// Cách dùng:
try {
throw new Error("Database timeout after 5000ms");
} catch (error) {
console.error(ensureErrorMessage(error));
}
Tại sao TypeScript lại thay đổi điều này?
Trước đây, catch (error) mặc định là any. Điều này rất nguy hiểm. Bạn có thể gọi error.something(), và compiler sẽ bỏ qua, để rồi ứng dụng của bạn bị lỗi TypeError trên môi trường production.
Flag useUnknownInCatchVariables buộc bạn phải thừa nhận rằng khối catch là một nơi "hỗn loạn" nơi bất cứ điều gì cũng có thể xảy ra. Nó có thể khiến bạn cảm thấy tốn thêm công sức lúc này, nhưng nó ngăn chặn các lỗi "Cannot read property of undefined" vốn thường gây nhức nhối cho các ứng dụng JavaScript lớn.
Cách xác minh bản sửa lỗi
- Kiểm tra Editor của bạn: Đường gạch chân màu đỏ trên
error.messagesẽ biến mất ngay khi bạn bao bọc nó trong một kiểm trainstanceof. - Chạy Build: Thực thi lệnh
npx tsc --noEmittrong terminal của bạn. Nếu bản sửa lỗi chính xác, lỗi TS18046 sẽ không còn xuất hiện trong kết quả đầu ra. - Kiểm tra các trường hợp biên (Edge Cases): Thử throw một chuỗi thủ công (
throw "API Key Missing") để đảm bảo logicelsecủa bạn xử lý nó mà không bị crash.
Mẹo chuyên nghiệp để quản lý lỗi
- Cưỡng lại sự cám dỗ của 'any': Đừng sử dụng
catch (error: any). Đó là một lối tắt bỏ qua chính các tính năng an toàn mà bạn đang sử dụng TypeScript để có được. - Giữ lại ngữ cảnh: Khi thu hẹp lỗi, hãy ghi lại đối tượng
unknowngốc vào các dịch vụ như Sentry. Nó thường chứa các metadata ẩn giúp ích cho việc debug. - Cấu hình: Nếu hành vi này làm hỏng một dự án legacy đồ sộ, bạn có thể tạm thời vô hiệu hóa nó bằng cách đặt
"useUnknownInCatchVariables": falsetrong filetsconfig.jsoncủa mình, mặc dù việc sửa các kiểu dữ liệu luôn là bước đi tốt hơn về lâu dài.

