Type Narrowing & Type Guards

0/5 in this phase0/21 across the roadmap

📖 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 instances
  • in — checks for property existence
  • Truthiness checks — if (value) eliminates null, 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

codeTap to expand ⛶
1// typeof narrowing
2function padLeft(value: string | number, padding: string | number): string {
3 if (typeof padding === "number") {
4 return " ".repeat(padding) + value; // padding is number
5 }
6 return padding + value; // padding is string
7}
8
9// instanceof narrowing
10function formatDate(input: Date | string): string {
11 if (input instanceof Date) {
12 return input.toISOString(); // input is Date
13 }
14 return new Date(input).toISOString(); // input is string
15}
16
17// 'in' narrowing
18interface Bird { fly(): void; layEggs(): void; }
19interface Fish { swim(): void; layEggs(): void; }
20
21function move(animal: Bird | Fish) {
22 if ("fly" in animal) {
23 animal.fly(); // animal is Bird
24 } else {
25 animal.swim(); // animal is Fish
26 }
27}
28
29// Truthiness narrowing
30function printName(name: string | null | undefined) {
31 if (name) {
32 console.log(name.toUpperCase()); // name is string
33 }
34}
35
36// ⭐ Custom type guard with type predicate
37interface Cat { meow(): void; purr(): void; }
38interface Dog { bark(): void; fetch(): void; }
39
40function isCat(pet: Cat | Dog): pet is Cat {
41 return "meow" in pet;
42}
43
44function handlePet(pet: Cat | Dog) {
45 if (isCat(pet)) {
46 pet.purr(); // TS knows pet is Cat
47 } else {
48 pet.fetch(); // TS knows pet is Dog
49 }
50}
51
52// Type predicate for filtering
53interface 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}
64
65const mixedData: unknown[] = [
66 { id: 1, name: "Alice" },
67 null,
68 { id: 2 }, // missing name
69 { id: 3, name: "Charlie" }
70];
71const validUsers: User[] = mixedData.filter(isValidUser);
72// Type-safe: [{ id: 1, name: "Alice" }, { id: 3, name: "Charlie" }]
73
74// Assertion function
75function 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 string
83}
84
85// Discriminated union narrowing (automatic!)
86type Result<T> =
87 | { ok: true; value: T }
88 | { ok: false; error: Error };
89
90function 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:

  1. Write type guards for typeof, instanceof, and in operators
  2. Create a custom type predicate isString(value: unknown): value is string
  3. Use a type predicate with .filter() to filter an array of mixed types
  4. Write an assertion function that asserts a value is a non-empty array
  5. 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 is predicate unconditionally; if it returns true incorrectly, you'll have runtime bugs

  • Using as type assertions instead of type guards — assertions bypass safety; guards prove safety

  • Not handling the else branch in narrowing — always consider what type remains after the check

  • Truthiness narrowing eliminates 0 and "" along with null/undefined — use explicit null checks if 0 or "" are valid values

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Type Narrowing & Type Guards