Type guard là một phép kiểm tra lúc chạy mà TypeScript hiểu được, nhờ đó nó thu hẹp kiểu bên trong nhánh đã kiểm tra. typeof, instanceof và in là các guard dựng sẵn; với mọi trường hợp khác, bạn viết một hàm có kiểu trả về là type predicate, value is Type.
Lúc chạy, isUser trả về một boolean bình thường. Kiểu trả về value is User cho trình biên dịch biết kết quả true chứng minh được điều gì, và sau đó mọi if (isUser(x)) đều thu hẹp x thành User.
Các type guard dựng sẵn
Những phép kiểm tra này thu hẹp kiểu mà không cần hàm hỗ trợ nào:
| Guard | Ví dụ | Dùng cho |
|---|---|---|
typeof | typeof x === "number" | kiểu nguyên thủy và hàm |
instanceof | x instanceof Date | instance của class |
in | "email" in x | union đối tượng, thuộc tính của đối tượng unknown |
Array.isArray | Array.isArray(x) | mảng |
| So sánh bằng | x === null, x.kind === "circle" | null/undefined, nhãn literal |
| Truthiness | if (x) | loại bỏ null và undefined |
Tất cả đều chạy như JavaScript bình thường. Điều TypeScript thêm vào là việc thu hẹp kiểu: nó đọc phép kiểm tra và điều chỉnh kiểu trong từng nhánh. Danh sách đầy đủ các dạng nằm ở trang thu hẹp kiểu. Guard tự viết dành cho những phép kiểm tra không gói gọn trong một biểu thức, hoặc những phép bạn muốn dùng lại.
Viết một type predicate
Type predicate có dạng parameterName is Type và thay cho boolean ở vị trí kiểu trả về. Việc thu hẹp hoạt động theo cả hai chiều: true thu hẹp thành Type, còn false loại Type khỏi union.
Truyền một guard cho filter sẽ cho ra một mảng có kiểu chính xác. Từ TypeScript 5.5, trình biên dịch còn tự suy ra predicate từ những arrow function đơn giản, nên pets.filter((p) => p.kind === "cat") trả về Cat[] mà không cần một guard có tên.
Kiểu của predicate phải khớp với kiểu của tham số: function f(x: string): x is number là lỗi TS2677, A type predicate's type must be assignable to its parameter's type.
Trình biên dịch tin guard của bạn
TypeScript kiểm tra rằng guard trả về một boolean. Nó không kiểm tra boolean đó có đúng không. Một guard trả về true cho giá trị sai sẽ khiến kiểu nói dối, và chương trình hỏng lúc chạy mà không có lỗi biên dịch nào.
data.price.toFixed(2) ném lỗi TypeError: Cannot read properties of undefined (reading 'toFixed') lúc chạy. Trình biên dịch chấp nhận data.price là number vì guard đã nói vậy. Hãy kiểm tra mọi thuộc tính mà phần code còn lại dựa vào, và giữ guard ngắn gọn, có test và đặt gần kiểu mà nó mô tả.
Kiểm tra một đối tượng có thuộc một kiểu không
Đây là câu hỏi đứng sau phần lớn các guard tự viết: dữ liệu đến dưới dạng unknown (từ JSON.parse, fetch, localStorage, một message) và bạn cần biết nó có khớp với interface của mình không. Công thức:
typeof value === "object" && value !== null(là đối tượng, không phảinull)."prop" in valuecho từng thuộc tính bắt buộc. Trênunknown,inthêm thuộc tính đó vào kiểu với kiểuunknown.typeof value.prop === "..."(hoặc một guard lồng nhau) cho kiểu của từng thuộc tính.Array.isArray(value.items) && value.items.every(isItem)cho mảng.
Với những cấu trúc lớn hoặc lồng sâu, viết tay các phép này rất mất công. Các thư viện schema như Zod hay Valibot cho bạn mô tả cấu trúc một lần và nhận về cả phép kiểm tra lúc chạy lẫn kiểu TypeScript.
Hàm assertion: asserts value is Type
Hàm assertion ném lỗi nếu kiểm tra thất bại, và trả về bình thường nếu thành công. Kiểu trả về của nó là asserts value is Type (hoặc asserts condition), và mọi thứ sau lời gọi đều được thu hẹp kiểu mà không cần if.
Có một quy tắc hay làm người ta vấp: hàm assertion phải được gọi qua một tên có kiểu tường minh. Một arrow function gán vào const mà không có chú thích kiểu, const check = (v: unknown): asserts v is string => {...}, gây lỗi TS2775 tại nơi gọi, Assertions require every name in the call target to be declared with an explicit type annotation. Hãy dùng khai báo function, hoặc chú thích hằng số đó bằng một kiểu hàm.
Guard, assertion và ép kiểu
| Công cụ | Kiểm tra lúc chạy? | Thu hẹp kiểu | Khi thất bại |
|---|---|---|---|
Guard dựng sẵn (typeof, in...) | Có | bên trong nhánh | đi vào nhánh còn lại |
Hàm value is T | Có (code của bạn) | bên trong nhánh | đi vào nhánh còn lại |
Hàm asserts value is T | Có (code của bạn) | sau lời gọi | ném lỗi |
value as T | Không | biểu thức đó | không có gì: kiểu sai lan ra |
Một type assertion (as) đổi kiểu mà không kiểm tra gì. Ở ranh giới nơi dữ liệu đến từ bên ngoài, guard hoặc hàm assertion là phiên bản an toàn của cùng ý tưởng đó.
Guard dựa trên this trong class
Một phương thức có thể thu hẹp kiểu của chính đối tượng gọi nó bằng this is Type. Cách này rất tiện trong hệ thống phân cấp class:
class FileNode {
constructor(public name: string) {}
isDirectory(): this is DirectoryNode {
return this instanceof DirectoryNode;
}
}
class DirectoryNode extends FileNode {
children: FileNode[] = [];
}
function count(node: FileNode): number {
return node.isDirectory() ? node.children.length : 0; // node: DirectoryNode in the true branch
}
Câu hỏi thường gặp
Type guard trong TypeScript là gì?
Là bất kỳ phép kiểm tra lúc chạy nào mà TypeScript dùng để thu hẹp kiểu: typeof x === "string", x instanceof Date, "id" in x, Array.isArray(x), hoặc lời gọi một hàm có kiểu trả về là type predicate như x is User. Bên trong nhánh đã kiểm tra, biến mang kiểu hẹp hơn.
Làm sao để kiểm tra một đối tượng có thuộc một kiểu trong TypeScript không?
Kiểu không tồn tại lúc chạy, nên bạn kiểm tra các thuộc tính: viết hàm isUser(value: unknown): value is User kiểm tra typeof value === "object", value !== null, và từng thuộc tính bắt buộc bằng in và typeof. Sau if (isUser(x)), x có kiểu User. Với class, chỉ cần x instanceof MyClass.
"value is Type" nghĩa là gì trong TypeScript?
Đó là một type predicate, dùng làm kiểu trả về của hàm. Lúc chạy hàm vẫn trả về boolean, nhưng khi nó trả về true, TypeScript thu hẹp đối số thành Type tại nơi gọi, còn khi trả về false thì thu hẹp thành các thành viên còn lại của union. Trình biên dịch không kiểm tra thân hàm, nên phép kiểm tra phải đúng.
Type guard và hàm assertion khác nhau thế nào?
Type guard (x is T) trả về boolean và thu hẹp kiểu bên trong một if. Hàm assertion (asserts x is T) không trả về gì và ném lỗi khi kiểm tra thất bại, nên mọi thứ sau lời gọi đều được thu hẹp mà không cần if. Dùng guard để rẽ nhánh và assertion cho trường hợp "điều này phải đúng, nếu không thì dừng".
Có thể kiểm tra một đối tượng có implement một interface trong TypeScript không?
Không trực tiếp được: interface bị xóa khi biên dịch và instanceof không nhận chúng. Hãy viết một type guard kiểm tra các thuộc tính của interface, hoặc thêm một thuộc tính nhãn dạng literal (kind: "user") rồi so sánh nó. Các thư viện schema như Zod tạo ra cả phép kiểm tra lẫn kiểu từ cùng một định nghĩa.