TypeScript narrows types through control-flow analysis: whenever you test a value, the compiler updates its type within each branch. A type guard is any expression that both performs a runtime check and narrows the type at compile time.
TypeScript thu hẹp kiểu (narrowing) bằng cách phân tích luồng điều khiển: bất cứ khi nào bạn kiểm tra một giá trị, trình biên dịch cập nhật kiểu của nó trong mỗi nhánh. Type guard là bất kỳ biểu thức nào thực hiện kiểm tra tại runtime và đồng thời thu hẹp kiểu tại compile time.
1type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };23function area(shape: Shape): number {4 // TypeScript knows shape is the circle variant here…5 if (shape.kind === "circle") {6 return Math.PI * shape.radius ** 2;7 }8 // …and the square variant here9 return shape.side ** 2;10}typeof and instanceof are built-in type guards. typeof works well with primitives; instanceof is useful for classes but cannot guard interface types because interfaces have no runtime representation.
typeof và instanceof là các type guard tích hợp sẵn. typeof hoạt động tốt với các kiểu nguyên thủy; instanceof hữu ích với các class, nhưng không hoạt động với các kiểu interface vì chúng không tồn tại ở runtime.
1// typeof — works for "string" | "number" | "boolean" | "bigint" | "symbol" | "undefined" | "function" | "object"2function pad(value: string | number, width: number): string {3 if (typeof value === "string") {4 return value.padStart(width); // value: string5 }6 return String(value).padStart(width); // value: number7}89// instanceof — narrows to a class type10class NetworkError extends Error {11 constructor(public statusCode: number, message: string) {12 super(message);When typeof and instanceof fall short, write your own guard function with a return type of val is T. This is a type predicate — it tells TypeScript that if the function returns true, the parameter is of type T.
Khi typeof và instanceof không đủ, bạn có thể viết hàm guard riêng với kiểu trả về là 'val is T'. Đây được gọi là type predicate — nó nói với TypeScript rằng nếu hàm trả về true, tham số là kiểu T.
1interface Cat { meow(): void }2interface Dog { bark(): void }34// Without a predicate — TypeScript doesn't narrow the union5function isCatPlain(pet: Cat | Dog): boolean {6 return "meow" in pet;7}89// With a predicate — TypeScript narrows correctly10function isCat(pet: Cat | Dog): pet is Cat {11 return "meow" in pet;12}unknown thoroughly.Responsibility cho tính đúng đắn thuộc về bạn — TypeScript tin tưởng hoàn toàn vào type predicate. Nếu hàm guard của bạn sai, bạn sẽ có lỗi runtime mà trình biên dịch không phát hiện được. Hãy kiểm tra kỹ lưỡng mọi guard viết trên unknown.
An assertion function is a type guard variant that does not return a boolean — it throws if the condition is false. The return type is asserts val is T. After the call, TypeScript treats the value as narrowed for the rest of the enclosing scope.
Assertion function là biến thể của type guard không trả về boolean — thay vào đó nó throw nếu điều kiện sai. Kiểu trả về là 'asserts val is T'. Sau khi gọi assertion function, TypeScript coi giá trị đã được thu hẹp trong toàn bộ phạm vi còn lại.
1// Assertion function — throws on failure, narrows on success2function assertIsString(val: unknown): asserts val is string {3 if (typeof val !== "string") {4 throw new TypeError(`Expected string, got ${typeof val}`);5 }6}78// asserts condition — truthy assertion without a type9function assert(condition: unknown, msg?: string): asserts condition {10 if (!condition) throw new Error(msg ?? "Assertion failed");11}12A discriminated union — also called a tagged union — is the most powerful narrowing pattern. Each variant carries a shared literal-type field; TypeScript uses that field to narrow the union without any external guard function.
Discriminated union (hay tagged union) là pattern mạnh mẽ nhất cho narrowing. Mỗi biến thể mang một literal type field chung — TypeScript dùng field này để thu hẹp union mà không cần hàm guard bên ngoài.
1type Result<T, E = Error> =2 | { ok: true; value: T }3 | { ok: false; error: E };45function divide(a: number, b: number): Result<number, string> {6 if (b === 0) return { ok: false, error: "Division by zero" };7 return { ok: true, value: a / b };8}910const result = divide(10, 2);1112if (result.ok) {The in operator checks property existence at runtime and simultaneously narrows the union at compile time. It is especially useful when interfaces have no shared discriminant field.
Toán tử in kiểm tra sự tồn tại của một property trên đối tượng ở runtime và đồng thời thu hẹp union ở compile time. Nó đặc biệt hữu ích khi các interface không có discriminant field chung.
1interface Fish { swim(): void }2interface Bird { fly(): void }3interface Amphibian { swim(): void; breatheAir(): void }45type Animal = Fish | Bird | Amphibian;67function move(animal: Animal): void {8 if ("fly" in animal) {9 animal.fly(); // animal: Bird10 } else if ("breatheAir" in animal) {11 animal.breatheAir(); // animal: Amphibian12 animal.swim();TypeScript tracks the type of each variable across every branch of control flow — reassignments, early returns, throws, and guards are all analysed. The type at any given point is the union of all possible types from every code path leading there.
TypeScript theo dõi kiểu của mỗi biến qua mọi nhánh của luồng điều khiển — gán lại, early return, throw, và guard đều được phân tích. Kiểu ở một điểm nhất định là union của tất cả các kiểu khả thi từ mọi đường dẫn code dẫn đến đó.
1function processValue(value: string | number | null | undefined): string {2 // After these guards value: string | number (null and undefined eliminated)3 if (value == null) throw new Error("value is required");45 // value: string | number6 if (typeof value === "number") {7 return value.toFixed(2); // value: number8 }910 // value: string (number branch returned above)11 return value.trim();12}TypeScript phân tích luồng điều khiển dựa trên lý thuyết lattice và dominance analysis. Điều này có nghĩa là nó hoạt động tốt với các pattern bình thường nhưng có thể bị thất bại với các aliased function — TypeScript không phân tích lại kiểu sau khi gọi một hàm tùy ý, vì hàm đó có thể thay đổi giá trị.
When you narrow a union until all variants are handled, TypeScript infers the remaining type as never. You can leverage this for compile-time exhaustiveness checks — add a new union variant and forget to handle it, and TypeScript reports an error immediately.
Khi bạn thu hẹp một union đến mức tất cả các trường hợp đã được xử lý, TypeScript suy ra kiểu là never. Bạn có thể tận dụng điều này để tạo các kiểm tra exhaustiveness tại compile time — nếu bạn thêm một biến thể mới vào union mà quên xử lý nó, TypeScript sẽ báo lỗi ngay lập tức.
1type Shape =2 | { kind: "circle"; radius: number }3 | { kind: "rectangle"; width: number; height: number }4 | { kind: "triangle"; base: number; height: number };56// Pattern 1: throw — catches unhandled cases at runtime7function area(shape: Shape): number {8 switch (shape.kind) {9 case "circle":10 return Math.PI * shape.radius ** 2;11 case "rectangle":12 return shape.width * shape.height;typeof and instanceof are built-in guards for primitives and classestypeof và instanceof là built-in guard cho kiểu nguyên thủy và class
Type predicate (val is T) cho phép viết guard tùy chỉnh; trình biên dịch tin tưởng hoàn toàn — hãy kiểm tra kỹ
Assertion function (asserts val is T) throw thay vì trả về false và thu hẹp kiểu trong phạm vi còn lại
Discriminated union với literal type tag là pattern narrowing mạnh mẽ và dễ bảo trì nhất
in operator narrows by property existence — no shared discriminant field neededToán tử in thu hẹp union dựa trên sự tồn tại của property — không cần discriminant field chung
Control flow analysis theo dõi kiểu qua mọi nhánh, gán lại, và early return
never gives compile-time assurance that every union variant is handledExhaustiveness checking với never đảm bảo tại compile time rằng mọi biến thể của union đều được xử lý