TypeScript Type Narrowing
TypeScript type narrowing is how the compiler figures out a more specific type inside a block of code. After a check such as typeof x === "string", TypeScript knows x is a string in that branch.
typeof and Truthiness Narrowing
Each early return removes a type from the union, so the final line knows value is a number.
function describe(value: string | number | null) {
if (value === null) return "nothing";
if (typeof value === "string") return `text of length ${value.length}`;
return `number ${value.toFixed(1)}`;
}
console.log(describe("hello"));
console.log(describe(7));
console.log(describe(null));text of length 5 number 7.0 nothing
instanceof and in
instanceof narrows to a class. The in operator checks whether a property exists, narrowing between object types.
type Fish = { swim: () => string };
type Bird = { fly: () => string };
function move(animal: Fish | Bird) {
return "swim" in animal ? animal.swim() : animal.fly();
}
console.log(move({ fly: () => "flying" }));
function when(d: Date | string) {
return d instanceof Date ? d.getFullYear() : d;
}
console.log(when(new Date("2026-05-07")), when("unknown"));flying 2026 unknown
Discriminated Unions
Give each type in a union a common literal property, often called kind or type. Checking it narrows the whole object. This pattern is ideal for API states, Redux actions and shapes.
type Result =
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; message: string };
function render(r: Result): string {
switch (r.status) {
case "loading": return "Loading...";
case "success": return `${r.data.length} items`;
case "error": return "Error: " + r.message;
}
}
console.log(render({ status: "success", data: ["a", "b"] }));
console.log(render({ status: "error", message: "timeout" }));2 items Error: timeout
Custom Type Guards
A function returning value is Type is a type predicate. When it returns true, TypeScript narrows the argument.
type User = { name: string; email: string };
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null && "name" in x && "email" in x;
}
const data: unknown = JSON.parse('{"name":"Noor","email":"noor@example.com"}');
if (isUser(data)) console.log(data.email);noor@example.com
Exhaustive Checks with never
Assigning the leftover value to never makes the compiler complain if you later add a new union member and forget to handle it.
type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "square": return s.side ** 2;
default: {
const unreachable: never = s; // error here if a case is missing
return unreachable;
}
}
}
console.log(area({ kind: "square", side: 4 }));16
Frequently asked questions
What is a type guard in TypeScript?
Any check that narrows a type, like typeof, instanceof or in, or a custom function with a return type of the form x is Type.
What is a discriminated union?
A union of object types that all share a literal property (the discriminant). Checking that property tells TypeScript exactly which member you have.
Why use never for exhaustive checks?
If every case is handled, the remaining type is never and the assignment compiles. Add a new member without handling it and you get a compile error.