unknown, any và never trong TypeScript: dùng cái nào
1. Tổng quan
TypeScript có ba kiểu đặc biệt dễ gây nhầm lẫn: any, unknown và never. Cả ba đều xuất hiện ở các “biên” của hệ thống kiểu. any tắt kiểm tra kiểu, unknown buộc bạn kiểm tra trước khi dùng, còn never biểu diễn trường hợp không thể xảy ra.
Đọc xong bài này, bạn sẽ biết chọn kiểu nào cho dữ liệu chưa rõ nguồn gốc. Bạn cũng sẽ biết cách dùng never để compiler báo lỗi khi bạn quên xử lý một nhánh.
Tất cả ví dụ chạy với TypeScript 5.x và "strict": true trong tsconfig.json.
2. any: tắt type checker
any cho phép một giá trị làm bất cứ thứ gì. Compiler không kiểm tra gì cả:
let payload: any = "xin chào";
payload.toUpperCase(); // OK
payload.foo.bar(); // OK lúc compile, lỗi lúc chạy
payload = 42;
const total: number = payload * 2; // OK
Dòng thứ ba biên dịch bình thường. Khi chạy, nó ném lỗi:
TypeError: Cannot read properties of undefined (reading 'bar')
Với any, TypeScript coi như bạn đã tự chịu trách nhiệm. Bạn mất autocomplete, mất kiểm tra tên thuộc tính, và mất lợi ích của refactor an toàn.
2.1. any lây lan
Vấn đề lớn nhất của any là nó lây sang mọi thứ chạm vào nó:
function loadConfig(): any {
return JSON.parse('{"port": "8080"}');
}
const config = loadConfig();
const port = config.port; // port: any
const next = port + 1; // next: any
console.log(next);
80801
port là chuỗi "8080", nên port + 1 cho ra "80801" thay vì 8081. Compiler không cảnh báo, vì any đã truyền qua config, port rồi next. Một chữ any ở đầu có thể làm mất kiểm tra kiểu ở cả một chuỗi lời gọi hàm.
Lưu ý:
JSON.parse,response.json()trong fetch API và nhiều thư viện cũ trả vềany. Đây là nguồn rò rỉanyphổ biến nhất trong code thực tế.
3. unknown: an toàn khi chưa biết kiểu
unknown cũng nhận được mọi giá trị như any. Điểm khác là bạn không được dùng giá trị đó cho tới khi đã thu hẹp kiểu (narrowing):
let payload: unknown = "xin chào";
payload.toUpperCase();
error TS18046: 'payload' is of type 'unknown'.
Compiler chặn ngay. Bạn phải chứng minh payload là chuỗi trước:
if (typeof payload === "string") {
console.log(payload.toUpperCase()); // payload: string
}
XIN CHÀO
Bên trong khối if, TypeScript biết payload là string, nên toUpperCase() hợp lệ.
3.1. Các cách thu hẹp unknown
Có bốn công cụ chính:
class PaymentError extends Error {
constructor(message: string, public readonly code: string) {
super(message);
}
}
function describe(value: unknown): string {
if (typeof value === "number") {
return `Số: ${value.toFixed(2)}`;
}
if (value instanceof PaymentError) {
return `Lỗi thanh toán ${value.code}`;
}
if (typeof value === "object" && value !== null && "id" in value) {
return `Object có id: ${String(value.id)}`;
}
return "Không xác định";
}
console.log(describe(12.5));
console.log(describe(new PaymentError("Hết tiền", "INSUFFICIENT")));
console.log(describe({ id: 7 }));
console.log(describe(null));
Số: 12.50
Lỗi thanh toán INSUFFICIENT
Object có id: 7
Không xác định
typeofdùng cho kiểu nguyên thủy:string,number,boolean…instanceofdùng cho class.inkiểm tra thuộc tính tồn tại. Từ TypeScript 4.9, sau"id" in valuethìvaluecó kiểuobject & Record<"id", unknown>.- Phải kiểm tra
value !== nullvìtypeof null === "object".
3.2. Type guard cho dữ liệu từ bên ngoài
Với object phức tạp, bạn nên viết một type guard, tức là hàm trả về value is T:
interface Order {
id: string;
customerName: string;
total: number;
}
export function isOrder(value: unknown): value is Order {
if (typeof value !== "object" || value === null) return false;
const v = value as Record<string, unknown>;
return (
typeof v.id === "string" &&
typeof v.customerName === "string" &&
typeof v.total === "number"
);
}
export function parseOrder(json: string): Order {
const data: unknown = JSON.parse(json);
if (!isOrder(data)) {
throw new Error("Dữ liệu đơn hàng không hợp lệ");
}
return data; // data: Order
}
console.log(parseOrder('{"id":"A1","customerName":"Lan","total":250000}'));
{ id: 'A1', customerName: 'Lan', total: 250000 }
Điểm quan trọng là dòng const data: unknown = JSON.parse(json). Việc gán kiểu unknown một cách tường minh chặn any từ JSON.parse lan ra ngoài. Từ đây, compiler buộc bạn kiểm tra trước khi dùng data.
Trong dự án lớn, bạn có thể thay type guard viết tay bằng thư viện validate schema như Zod. Nguyên tắc vẫn giữ nguyên: dữ liệu vào là unknown, sau khi validate mới thành kiểu cụ thể.
3.3. unknown trong catch
Với strict: true (cụ thể là cờ useUnknownInCatchVariables), biến trong catch có kiểu unknown:
try {
parseOrder("{}");
} catch (err) {
// err: unknown
const message = err instanceof Error ? err.message : String(err);
console.log(message);
}
Dữ liệu đơn hàng không hợp lệ
Điều này đúng với thực tế. JavaScript cho phép throw bất cứ thứ gì, kể cả chuỗi hay số, nên bạn không thể giả định err luôn là Error.
4. never: kiểu không có giá trị
never là kiểu rỗng. Không có giá trị nào thuộc kiểu never. Nó xuất hiện ở ba tình huống chính.
4.1. Hàm không bao giờ trả về
Một hàm luôn ném lỗi, hoặc chạy vòng lặp vô hạn, có kiểu trả về never:
function fail(message: string): never {
throw new Error(message);
}
function getDiscount(code: string): number {
if (code === "TET2026") return 0.2;
return fail(`Mã giảm giá không tồn tại: ${code}`);
}
Vì never gán được cho mọi kiểu, return fail(...) hợp lệ trong hàm trả về number. Compiler cũng hiểu rằng code phía sau lời gọi fail() là không thể chạy tới.
4.2. Kiểm tra exhaustive
Đây là cách dùng never hữu ích nhất. Giả sử bạn có union các phương thức thanh toán:
type PaymentMethod =
| { kind: "card"; last4: string }
| { kind: "cash" }
| { kind: "momo"; phone: string };
export function assertNever(value: never): never {
throw new Error(`Trường hợp chưa xử lý: ${JSON.stringify(value)}`);
}
export function paymentLabel(method: PaymentMethod): string {
switch (method.kind) {
case "card":
return `Thẻ **** ${method.last4}`;
case "cash":
return "Tiền mặt";
case "momo":
return `MoMo ${method.phone}`;
default:
return assertNever(method);
}
}
Sau khi xử lý đủ ba case, trong nhánh default thì method bị thu hẹp thành never. Lời gọi assertNever(method) vì thế hợp lệ.
Bây giờ, giả sử đồng nghiệp thêm { kind: "zalopay"; phone: string } vào union nhưng quên sửa switch. Compiler báo ngay:
error TS2345: Argument of type '{ kind: "zalopay"; phone: string; }' is not assignable to parameter of type 'never'.
Lỗi được phát hiện lúc compile, thay vì để người dùng gặp nhãn undefined trên màn hình thanh toán. Nếu dữ liệu lạ vẫn lọt vào lúc chạy, ví dụ từ API cũ, assertNever sẽ ném lỗi rõ ràng.
4.3. never trong conditional type
Trong conditional type, never dùng để loại một thành viên khỏi union, vì A | never chính là A:
type OnlyStrings<T> = T extends string ? T : never;
type Mixed = "vnd" | "usd" | 404 | null;
type Currencies = OnlyStrings<Mixed>; // "vnd" | "usd"
TypeScript phân phối conditional type lên từng thành viên của union. Các thành viên 404 và null biến thành never rồi biến mất. Các utility type có sẵn như Exclude, Extract và NonNullable đều hoạt động theo cơ chế này.
5. So sánh nhanh
Khác biệt cốt lõi nằm ở quan hệ gán (assignability):
| Gán giá trị khác vào kiểu này | Gán kiểu này sang kiểu khác | Dùng trực tiếp (gọi method, truy cập thuộc tính) | |
|---|---|---|---|
any |
Được | Được (trừ never) |
Được, không kiểm tra |
unknown |
Được | Chỉ sang unknown, any |
Không, phải narrowing |
never |
Không (trừ never) |
Được, sang mọi kiểu | Không có giá trị để dùng |
Có thể tóm lại như sau:
unknownlà top type an toàn. Mọi thứ đều làunknown.neverlà bottom type.neverlà con của mọi kiểu.anylà lối thoát. Nó vừa đóng vai top type vừa đóng vai bottom type, và vì thế phá vỡ tính đúng đắn của hệ thống kiểu.
6. Kiểm thử hành vi
Bạn có thể kiểm tra cả hành vi lúc chạy lẫn kiểu lúc compile bằng Vitest. Cài đặt:
npm install -D vitest typescript
Giả sử các hàm ở mục 3.2 và 4.2 nằm trong order.ts và payment.ts:
// payment.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { isOrder, parseOrder } from "./order";
import { paymentLabel, assertNever } from "./payment";
describe("unknown và type guard", () => {
it("chấp nhận đơn hàng hợp lệ", () => {
const order = parseOrder('{"id":"A1","customerName":"Lan","total":1000}');
expect(order.total).toBe(1000);
expectTypeOf(order.total).toEqualTypeOf<number>();
});
it("từ chối dữ liệu sai kiểu", () => {
expect(isOrder({ id: "A1", customerName: "Lan", total: "1000" })).toBe(false);
expect(() => parseOrder("null")).toThrow("Dữ liệu đơn hàng không hợp lệ");
});
});
describe("never và exhaustive check", () => {
it("trả về nhãn đúng cho từng phương thức", () => {
expect(paymentLabel({ kind: "card", last4: "4242" })).toBe("Thẻ **** 4242");
expect(paymentLabel({ kind: "cash" })).toBe("Tiền mặt");
});
it("ném lỗi khi gặp dữ liệu ngoài union lúc chạy", () => {
const legacy = { kind: "paypal" } as unknown as never;
expect(() => assertNever(legacy)).toThrow("Trường hợp chưa xử lý");
});
});
npx vitest run
✓ payment.test.ts (4 tests)
Test Files 1 passed (1)
Tests 4 passed (4)
expectTypeOf kiểm tra kiểu ở compile time. Nếu một thay đổi nào đó làm order.total trở thành any, test sẽ thất bại khi chạy type check (vitest --typecheck). Test cuối mô phỏng dữ liệu “lạ” từ hệ thống cũ, để chắc chắn lưới an toàn lúc chạy vẫn hoạt động.
7. Lỗi hay gặp và khi nào không nên dùng
Dùng any để “cho qua” lỗi compile. Đây là thói quen tệ nhất. Nếu bạn thật sự chưa biết kiểu, hãy dùng unknown. Hãy bật rule @typescript-eslint/no-explicit-any để bắt các trường hợp này.
Ép kiểu unknown bằng as thay vì narrowing. data as Order chỉ trấn an compiler mà không kiểm tra gì lúc chạy. Về bản chất, nó nguy hiểm không kém any.
const data: unknown = JSON.parse('{"id": 1}');
const order = data as Order; // compile OK
order.customerName.trim(); // TypeError lúc chạy
Dùng unknown cho mọi thứ. unknown dành cho biên hệ thống: input người dùng, response API, catch, dữ liệu deserialize. Bên trong domain, hãy dùng kiểu cụ thể. Một hàm calculateTotal(items: unknown) là thiết kế kém.
Nhầm never với void. void nghĩa là hàm trả về nhưng không có giá trị hữu ích. never nghĩa là hàm không bao giờ trả về. Một hàm log() bình thường phải là void, không phải never.
Quên default trong switch. Thiếu assertNever, TypeScript chỉ báo lỗi nếu hàm có kiểu trả về tường minh và bật noImplicitReturns. Kể cả khi đó, bạn vẫn mất lưới an toàn lúc chạy.
Lưu ý:
anyvẫn có chỗ dùng hợp lý. Đó là khi migrate dần một codebase JavaScript lớn, hoặc khi viết declaration cho thư viện mà kiểu thực sự quá động. Hãy khoanh vùng nó thật hẹp và để lại comment giải thích lý do.
8. Kết luận
Ba kiểu này có ba vai trò rõ ràng:
- Dùng
unknowncho dữ liệu chưa rõ kiểu ở biên hệ thống, rồi thu hẹp bằngtypeof,instanceof,inhoặc type guard. - Dùng
nevercho hàm luôn ném lỗi, cho kiểm tra exhaustive vớiassertNever, và để lọc union trong conditional type. - Tránh
any, trừ khi bạn có lý do cụ thể và đã khoanh vùng nó.
Quy tắc thực tế rất đơn giản. Khi định gõ any, hãy thử unknown trước. Nếu compiler báo lỗi, đó chính là những chỗ bạn cần kiểm tra dữ liệu.