any và unknown đều nhận mọi giá trị. Điểm khác nhau là bạn làm được gì với giá trị đó sau này: any cho phép làm mọi thứ và không kiểm tra gì, còn unknown gần như không cho làm gì cho đến khi bạn chứng minh được giá trị đó là gì.
Nếu không có dòng @ts-expect-error, u.toUpperCase() là lỗi biên dịch TS18046. Phép kiểm tra typeof thu hẹp u thành string, và bên trong khối đó mọi phương thức của chuỗi đều dùng được.
So sánh nhanh any và unknown
any | unknown | |
|---|---|---|
| Nhận mọi giá trị | có | có |
Gán cho string, number, ... | có, không kiểm tra | không (TS2322) |
| Đọc thuộc tính, gọi phương thức | có, không kiểm tra | không (TS18046) |
| Gọi như một hàm | có | không |
Tính toán và so sánh (x * 2, x + 1, x < 5) | có | không (TS18046) |
| Cần kiểm tra trước khi dùng | không | có (typeof, instanceof, in, một type guard) |
| Ảnh hưởng đến kiểm tra kiểu | bị tắt cho giá trị đó và mọi thứ nó chạm tới | vẫn được giữ |
Theo lý thuyết kiểu, unknown là top type: mọi kiểu đều gán được cho nó, còn nó chỉ gán được cho unknown và any. any là một lối thoát gán được theo cả hai chiều, cho mọi kiểu trừ never.
any tắt kiểm tra kiểu
Một giá trị có kiểu any được tin tưởng mù quáng. Trình biên dịch chấp nhận lỗi gõ sai, sai kiểu và thiếu thuộc tính, và các lỗi đó sẽ lộ ra lúc runtime.
Kết quả cho thấy vấn đề: một biến được chú thích là number lại chứa chuỗi, và dòng cuối ném ra lỗi Cannot read properties of undefined (reading 'city'). any còn lây lan. user.name là any, nên mọi giá trị tính từ nó cũng là any, và chỉ một giá trị không có kiểu cũng có thể tắt kiểm tra ở những chỗ cách xa nơi nó đi vào.
unknown buộc bạn kiểm tra trước
Với unknown, trình biên dịch từ chối mọi thao tác cho đến khi code thu hẹp giá trị. Việc thu hẹp (narrowing) dùng các phép kiểm tra JavaScript thông thường, và bên trong mỗi nhánh, giá trị mang kiểu đã được kiểm tra.
Phép kiểm tra value === null phải đứng trước phép kiểm tra object vì typeof null là "object". Sau "id" in value, TypeScript biết object có thuộc tính id, với kiểu unknown, vì không có gì cho biết nó chứa gì. String() chuyển nó thành chuỗi một cách tường minh; để dùng nó như một số, bạn cần kiểm tra bằng typeof trước.
Bạn cũng có thể bỏ qua phép kiểm tra bằng một assertion, value as string, và trình biên dịch sẽ chấp nhận. Đó là một lời hứa không có kiểm tra nào lúc runtime đứng sau, nên hãy ưu tiên kiểm tra thật; xem type assertion.
Kiểm tra JSON với unknown
JSON.parse được khai báo trả về any, nên kết quả của nó âm thầm tắt kiểm tra. Hãy chú thích kết quả là unknown và viết một type guard kiểm tra cấu trúc trước khi phần còn lại của code tin tưởng nó.
Đầu vào đầu tiên in ra dark at 14px, đầu vào thứ hai bị từ chối vì thiếu fontSize. Nếu dùng any, đầu vào thứ hai sẽ lọt qua như một Settings với fontSize là undefined. Với schema lớn, một thư viện kiểm tra dữ liệu làm cùng việc này và tự suy ra kiểu cho bạn.
noImplicitAny
any không chỉ đến từ việc ai đó viết ra. Một tham số không có chú thích kiểu và không có ngữ cảnh để suy luận cũng sẽ là any. Tùy chọn noImplicitAny, được strict bật lên, báo đó là lỗi:
index.ts(2,17): error TS7006: Parameter 'x' implicitly has an 'any' type.
Cách sửa là thêm chú thích kiểu, function double(x: number). Viết x: any tường minh cũng biên dịch được, và đó chính là mục đích: any vẫn hiện rõ trong code, có thể tìm kiếm và review.
Những chỗ any vẫn lén lọt vào
Ngay cả với strict, những nguồn sau vẫn tạo ra any mà không có chữ đó trong code của bạn:
| Nguồn | Bạn nhận được | Cách xử lý |
|---|---|---|
JSON.parse(text) | any | chú thích là unknown, rồi kiểm tra |
response.json() với kiểu của trình duyệt (DOM) | Promise<any> | giống JSON.parse (kiểu fetch riêng của Node đã trả về Promise<unknown>) |
| Package không có định nghĩa kiểu | any cho các import của nó (kèm lỗi trừ khi bạn thêm khai báo) | cài @types/... hoặc viết tệp .d.ts |
value as any | any | dùng type guard hoặc một assertion chính xác |
catch (e) khi tắt useUnknownInCatchVariables | any | strict biến nó thành unknown; hãy giữ như vậy |
Biến trong catch là unknown khi bật strict vì bất cứ thứ gì cũng có thể bị ném ra, không chỉ đối tượng Error. Hãy thu hẹp nó bằng e instanceof Error trước khi đọc e.message.
Khi nào dùng any là chấp nhận được
any không bị cấm, nhưng mỗi chỗ dùng nó là một chỗ trình biên dịch không còn bảo vệ. Những trường hợp hợp lý:
- Chuyển đổi một codebase JavaScript, nơi
anyđánh dấu phần chưa có kiểu. - Code mà hệ thống kiểu khó diễn đạt, được giữ nhỏ và nằm sau một chữ ký hàm có kiểu rõ ràng.
- Code test cố tình truyền dữ liệu sai.
Với mọi trường hợp khác, unknown xử lý cùng tình huống "không biết kiểu của giá trị" mà vẫn giữ các phép kiểm tra. Nhiều nhóm áp dụng điều này bằng quy tắc lint @typescript-eslint/no-explicit-any. Một kiểu liên quan, Record<string, unknown>, là lựa chọn thường dùng cho "một object có các giá trị không rõ kiểu".
Câu hỏi thường gặp
any và unknown trong TypeScript khác nhau thế nào?
Cả hai đều nhận mọi giá trị. Với any, bạn làm gì với giá trị cũng được (đọc thuộc tính, gọi nó, gán nó cho một number) và trình biên dịch không kiểm tra gì cả. Với unknown, bạn gần như không làm được gì cho đến khi thu hẹp nó bằng một phép kiểm tra như typeof x === "string". unknown vẫn giữ kiểm tra kiểu, vì vậy nó là lựa chọn an toàn hơn.
Khi nào nên dùng unknown thay cho any?
Bất cứ khi nào kiểu của giá trị không biết được lúc biên dịch: JSON đã parse, dữ liệu từ response mạng, lỗi bắt được trong catch, đầu vào của một hàm kiểm tra dữ liệu. Đặt kiểu là unknown rồi thu hẹp nó. Chỉ dùng any cho việc chuyển đổi code tạm thời hoặc code mà hệ thống kiểu không mô tả được.
Lỗi "Object is of type 'unknown'" nghĩa là gì?
Lỗi TS18046 ('x' is of type 'unknown') và TS2571 (Object is of type 'unknown', dùng khi giá trị không phải là một tên đơn giản, ví dụ load().id) nghĩa là bạn đã dùng một giá trị unknown như thể nó có một kiểu cụ thể, chẳng hạn đọc thuộc tính hoặc gọi phương thức. Hãy kiểm tra kiểu trước (typeof, instanceof, Array.isArray, in, hoặc một hàm type guard), rồi dùng giá trị bên trong nhánh đã được thu hẹp.
Vì sao JSON.parse trả về any?
Khai báo của nó trong thư viện chuẩn ghi parse(text: string, ...): any, vì trình biên dịch không thể biết một chuỗi chứa gì. Kết quả âm thầm tắt kiểm tra cho mọi thứ nó chạm tới. Hãy chú thích kiểu thay vào đó: const data: unknown = JSON.parse(text), rồi kiểm tra trước khi dùng.
noImplicitAny là gì?
Một tùy chọn trình biên dịch, nằm trong strict, báo lỗi khi một khai báo sẽ âm thầm nhận kiểu any vì không có chú thích kiểu và không có gì để suy luận: TS7006 cho tham số, TS7005 hoặc TS7034 cho biến không xác định được kiểu. Nó ngăn any xuất hiện mà không ai viết ra. any viết tường minh vẫn được phép.