TypeScript Type Narrowing

Type narrowing is the process of reducing a broad type to a more specific one based on conditions in the code. When a variable can be a string or a number, TypeScript does not know which one to work with until the code checks it. Inside that check, TypeScript automatically "narrows" the type to the one that matches — allowing safe access to type-specific methods and properties.

Why Narrowing Exists

BEFORE NARROWING:
  value: string | number
  └── TypeScript: "It could be either — I won't let you use string methods yet"

AFTER NARROWING (inside an if-block):
  if (typeof value === "string") {
      value   ← TypeScript: "Now I know it's a string"
      value.toUpperCase() ← allowed
  }

typeof Narrowing

The typeof operator is the most common narrowing tool. TypeScript recognizes typeof checks and narrows the type inside the matching block.

function formatValue(value: string | number | boolean): string {
    if (typeof value === "string") {
        return value.toUpperCase();        // string methods available
    }
    if (typeof value === "number") {
        return value.toFixed(2);           // number methods available
    }
    if (typeof value === "boolean") {
        return value ? "YES" : "NO";       // boolean usage
    }
    // TypeScript knows no other type can reach here
    return "";
}

console.log(formatValue("hello"));   // HELLO
console.log(formatValue(3.14159));   // 3.14
console.log(formatValue(true));      // YES

Equality Narrowing

Comparing a value with === or !== also narrows the type.

function handleStatus(status: "success" | "error" | "pending"): void {
    if (status === "success") {
        console.log("Operation completed!");
    } else if (status === "error") {
        console.log("Something went wrong.");
    } else {
        // TypeScript knows: only "pending" remains here
        console.log("Still processing...");
    }
}

in Operator Narrowing

The in operator checks if a property exists on an object. TypeScript narrows the type to the object type that has that property.

interface Car {
    make: string;
    numberOfWheels: number;
}

interface Bicycle {
    brand: string;
    gearCount: number;
}

function describeVehicle(vehicle: Car | Bicycle): void {
    if ("make" in vehicle) {
        // TypeScript: vehicle is Car here
        console.log(`Car: ${vehicle.make} with ${vehicle.numberOfWheels} wheels`);
    } else {
        // TypeScript: vehicle is Bicycle here
        console.log(`Bicycle: ${vehicle.brand} with ${vehicle.gearCount} gears`);
    }
}

describeVehicle({ make: "Toyota", numberOfWheels: 4 });
// Car: Toyota with 4 wheels

describeVehicle({ brand: "Trek", gearCount: 21 });
// Bicycle: Trek with 21 gears

instanceof Narrowing

The instanceof operator checks if an object was created from a specific class. TypeScript narrows to that class inside the block.

class Teacher {
    constructor(public name: string, public subject: string) {}

    getRole(): string {
        return `${this.name} teaches ${this.subject}`;
    }
}

class Principal {
    constructor(public name: string, public schoolName: string) {}

    getRole(): string {
        return `${this.name} heads ${this.schoolName}`;
    }
}

function introduce(person: Teacher | Principal): void {
    if (person instanceof Teacher) {
        // TypeScript: person is Teacher here
        console.log(person.getRole());
        console.log(`Subject: ${person.subject}`);
    } else {
        // TypeScript: person is Principal here
        console.log(person.getRole());
        console.log(`School: ${person.schoolName}`);
    }
}

introduce(new Teacher("Ms. Rao", "Mathematics"));
// Ms. Rao teaches Mathematics
// Subject: Mathematics

introduce(new Principal("Mr. Kapoor", "GovtSchool"));
// Mr. Kapoor heads GovtSchool
// School: GovtSchool

Narrowing Flow Diagram

value: string | number | null
           │
    ┌──────┴──────────────────────────────┐
    │                                     │
typeof === "string"          typeof === "number"
    │                                     │
  string                               number
(use .toUpperCase)            (use .toFixed, arithmetic)
                │
          null check
          value !== null
                │
             handled

User-Defined Type Guards

A type guard function returns a boolean but with a special return type param is Type. TypeScript uses this to narrow the type inside an if block that calls the function.

interface Cat {
    meow(): void;
}

interface Dog {
    bark(): void;
}

// Type guard function
function isDog(animal: Cat | Dog): animal is Dog {
    return (animal as Dog).bark !== undefined;
}

function makeNoise(animal: Cat | Dog): void {
    if (isDog(animal)) {
        animal.bark();    // TypeScript knows it's a Dog here
    } else {
        animal.meow();    // TypeScript knows it's a Cat here
    }
}

let pet: Dog = { bark: () => console.log("Woof!") };
makeNoise(pet);  // Woof!

Discriminated Unions

A discriminated union is a union of types that each have a common literal property (called the discriminant). TypeScript uses that property to narrow automatically — no extra checks needed.

type Circle    = { shape: "circle";    radius: number };
type Rectangle = { shape: "rectangle"; width: number; height: number };
type Triangle  = { shape: "triangle";  base: number;  height: number };

type Shape = Circle | Rectangle | Triangle;

function getArea(s: Shape): number {
    switch (s.shape) {           // "shape" is the discriminant
        case "circle":
            return Math.PI * s.radius * s.radius;  // s is Circle
        case "rectangle":
            return s.width * s.height;             // s is Rectangle
        case "triangle":
            return 0.5 * s.base * s.height;        // s is Triangle
    }
}

console.log(getArea({ shape: "circle",    radius: 7 }));         // 153.94
console.log(getArea({ shape: "rectangle", width: 8, height: 5 })); // 40
console.log(getArea({ shape: "triangle",  base: 6, height: 4 }));  // 12

Narrowing Methods Summary

MethodSyntaxBest For
typeoftypeof x === "string"Primitives: string, number, boolean
instanceofx instanceof ClassNameClass instances
in"property" in objInterfaces / objects with unique properties
Equalityx === "value"Literal types and union of literals
Null checkx !== nullNullable types
Type guard functionx is TypeComplex custom checks
Discriminated unionCommon literal propertyUnion of related object types

Leave a Comment

Your email address will not be published. Required fields are marked *