Type Narrowing & Type Guards
📖 Concept
Type narrowing is the process by which TypeScript refines a broad type to a more specific one inside a code block. It's one of TypeScript's most powerful features — the compiler tracks your control flow and automatically narrows types.
Built-in narrowing:
typeof— checks primitive types ("string","number","boolean", etc.)instanceof— checks class instancesin— checks for property existence- Truthiness checks —
if (value)eliminatesnull,undefined,0,"" - Equality checks —
===,!==,==,!=
Custom type guards use type predicates (param is Type) to tell TypeScript: "if this function returns true, the parameter is this type."
Assertion functions (asserts param is Type) throw if the condition fails, and TypeScript narrows the type after the call.
Discriminated unions narrow automatically when you check the discriminant property — this is the most common and powerful narrowing pattern in production TypeScript.
🏠 Real-world analogy: Type narrowing is like airport security checkpoints. At the entrance, anyone could be anything. After you show your passport (typeof check), they know you're a citizen. After the metal detector (property check), they know you aren't carrying weapons. Each checkpoint narrows who you could be.
💻 Code Example
1// typeof narrowing2function padLeft(value: string | number, padding: string | number): string {3 if (typeof padding === "number") {4 return " ".repeat(padding) + value; // padding is number5 }6 return padding + value; // padding is string7}89// instanceof narrowing10function formatDate(input: Date | string): string {11 if (input instanceof Date) {12 return input.toISOString(); // input is Date13 }14 return new Date(input).toISOString(); // input is string15}1617// 'in' narrowing18interface Bird { fly(): void; layEggs(): void; }19interface Fish { swim(): void; layEggs(): void; }2021function move(animal: Bird | Fish) {22 if ("fly" in animal) {23 animal.fly(); // animal is Bird24 } else {25 animal.swim(); // animal is Fish26 }27}2829// Truthiness narrowing30function printName(name: string | null | undefined) {31 if (name) {32 console.log(name.toUpperCase()); // name is string33 }34}3536// ⭐ Custom type guard with type predicate37interface Cat { meow(): void; purr(): void; }38interface Dog { bark(): void; fetch(): void; }3940function isCat(pet: Cat | Dog): pet is Cat {41 return "meow" in pet;42}4344function handlePet(pet: Cat | Dog) {45 if (isCat(pet)) {46 pet.purr(); // TS knows pet is Cat47 } else {48 pet.fetch(); // TS knows pet is Dog49 }50}5152// Type predicate for filtering53interface User { id: number; name: string; }54function isValidUser(user: unknown): user is User {55 return (56 typeof user === "object" &&57 user !== null &&58 "id" in user &&59 "name" in user &&60 typeof (user as User).id === "number" &&61 typeof (user as User).name === "string"62 );63}6465const mixedData: unknown[] = [66 { id: 1, name: "Alice" },67 null,68 { id: 2 }, // missing name69 { id: 3, name: "Charlie" }70];71const validUsers: User[] = mixedData.filter(isValidUser);72// Type-safe: [{ id: 1, name: "Alice" }, { id: 3, name: "Charlie" }]7374// Assertion function75function assertDefined<T>(value: T | null | undefined, msg?: string): asserts value is T {76 if (value === null || value === undefined) {77 throw new Error(msg ?? "Value is null or undefined");78 }79}80function processOrder(orderId: string | null) {81 assertDefined(orderId, "Order ID is required");82 console.log(orderId.toUpperCase()); // TS knows orderId is string83}8485// Discriminated union narrowing (automatic!)86type Result<T> =87 | { ok: true; value: T }88 | { ok: false; error: Error };8990function unwrap<T>(result: Result<T>): T {91 if (result.ok) {92 return result.value; // TS narrows: { ok: true; value: T }93 }94 throw result.error; // TS narrows: { ok: false; error: Error }95}
🏋️ Practice Exercise
Mini Exercise:
- Write type guards for
typeof,instanceof, andinoperators - Create a custom type predicate
isString(value: unknown): value is string - Use a type predicate with
.filter()to filter an array of mixed types - Write an assertion function that asserts a value is a non-empty array
- Create a discriminated union for API responses and handle each case with automatic narrowing
⚠️ Common Mistakes
Relying only on
typeof— it cannot distinguish objects (typeof {} === typeof [] === typeof null === 'object')Forgetting that type predicates lie if implemented wrong — TypeScript trusts your
ispredicate unconditionally; if it returns true incorrectly, you'll have runtime bugsUsing
astype assertions instead of type guards — assertions bypass safety; guards prove safetyNot handling the
elsebranch in narrowing — always consider what type remains after the checkTruthiness narrowing eliminates
0and""along withnull/undefined— use explicit null checks if0or""are valid values
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Type Narrowing & Type Guards