TypeScript Type Guards

A type guard is a condition that narrows a value's type to something more specific. When a variable holds a union type (like string | number), TypeScript cannot safely call type-specific methods without first checking which type the value actually is. Type guards perform that check, and TypeScript uses the result to narrow the type inside each branch of the condition.

Why Type Guards Are Needed

  Variable: number | string
            |
            v
  TypeScript sees a union — no type-specific methods available yet
            |
    +-------+-------+
    |               |
  Check type      Use without check
    |               |
  Narrowed ✓      Error ✗ — TypeScript blocks it

  Example:
  input.toUpperCase(); // Error — method only exists on string, not number
  if (typeof input === "string") {
      input.toUpperCase(); // Valid — TypeScript knows it's string here
  }

typeof Type Guard

The typeof operator checks the primitive type of a value. TypeScript understands typeof checks and narrows the type inside the condition block.

function describeValue(value: number | string | boolean): string {
    if (typeof value === "string") {
        return "String with length: " + value.length;
    }
    if (typeof value === "number") {
        return "Number squared: " + value * value;
    }
    return "Boolean: " + value;
}

console.log(describeValue("TypeScript")); // String with length: 10
console.log(describeValue(7));            // Number squared: 49
console.log(describeValue(true));         // Boolean: true

instanceof Type Guard

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

class Car {
    drive(): void { console.log("Car is driving"); }
}

class Boat {
    sail(): void { console.log("Boat is sailing"); }
}

function move(vehicle: Car | Boat): void {
    if (vehicle instanceof Car) {
        vehicle.drive(); // Narrowed to Car
    } else {
        vehicle.sail();  // Narrowed to Boat
    }
}

move(new Car());  // Car is driving
move(new Boat()); // Boat is sailing

in Operator Type Guard

The in operator checks if a property exists in an object. TypeScript uses this to distinguish between object types that share some properties but have different unique ones.

type Bird = {
    name: string;
    fly(): void;
    wingSpan: number;
};

type Fish = {
    name: string;
    swim(): void;
    finCount: number;
};

function move(animal: Bird | Fish): void {
    if ("fly" in animal) {
        // Narrowed to Bird — has the 'fly' property
        console.log(animal.name + " flies with span " + animal.wingSpan + "m");
    } else {
        // Narrowed to Fish — has the 'swim' property
        console.log(animal.name + " swims with " + animal.finCount + " fins");
    }
}

move({ name: "Eagle", fly() {}, wingSpan: 2.1 });
// Eagle flies with span 2.1m

move({ name: "Tuna", swim() {}, finCount: 7 });
// Tuna swims with 7 fins

Equality Narrowing

Comparing a value to a specific literal (using === or !==) narrows the type to that literal value inside the condition.

type Status = "active" | "inactive" | "pending";

function getStatusMessage(status: Status): string {
    if (status === "active") {
        return "Account is currently active.";
    }
    if (status === "inactive") {
        return "Account has been deactivated.";
    }
    return "Account is awaiting approval."; // status must be "pending" here
}

console.log(getStatusMessage("active"));   // Account is currently active.
console.log(getStatusMessage("pending"));  // Account is awaiting approval.

User-Defined Type Guards

A user-defined type guard is a function that returns a type predicate — a special return type of the form parameter is Type. When this function returns true, TypeScript narrows the parameter to the specified type.

type Cat = { meow(): void; lives: number; };
type Dog = { bark(): void; breed: string; };

// Type predicate function
function isCat(animal: Cat | Dog): animal is Cat {
    return (animal as Cat).meow !== undefined;
}

function interact(animal: Cat | Dog): void {
    if (isCat(animal)) {
        console.log("Cat with " + animal.lives + " lives");
        animal.meow();
    } else {
        console.log("Dog breed: " + animal.breed);
        animal.bark();
    }
}

let myCat: Cat = { lives: 9, meow() { console.log("Meow!"); } };
let myDog: Dog = { breed: "Labrador", bark() { console.log("Woof!"); } };

interact(myCat); // Cat with 9 lives → Meow!
interact(myDog); // Dog breed: Labrador → Woof!

Assertion Functions

An assertion function throws an error if a condition is false and tells TypeScript to narrow the type after the call. Use the asserts keyword in the return type.

function assertIsString(value: unknown): asserts value is string {
    if (typeof value !== "string") {
        throw new Error("Expected string, got: " + typeof value);
    }
}

let data: unknown = "Hello";
assertIsString(data);
// After this line, TypeScript knows 'data' is a string
console.log(data.toUpperCase()); // HELLO

Type Guard Methods Summary

Type GuardSyntaxBest For
typeoftypeof x === "string"Primitives (string, number, boolean)
instanceofx instanceof ClassNameClass instances
in"prop" in objObject types with unique properties
Equalityx === "value"Literal union types
Type predicatex is Type returnCustom complex checks
Assertion functionasserts x is TypeThrowing on invalid types

Practical Example

// Payment processing with type guards
type CreditCardPayment = {
    method: "credit";
    cardNumber: string;
    expiryDate: string;
    cvv: string;
};

type UPIPayment = {
    method: "upi";
    upiId: string;
};

type NetBankingPayment = {
    method: "netbanking";
    bankName: string;
    accountNumber: string;
};

type Payment = CreditCardPayment | UPIPayment | NetBankingPayment;

function processPayment(payment: Payment, amount: number): void {
    console.log("Processing ₹" + amount + " via " + payment.method.toUpperCase());

    switch (payment.method) {
        case "credit":
            console.log("Card ending: " + payment.cardNumber.slice(-4));
            break;
        case "upi":
            console.log("UPI ID: " + payment.upiId);
            break;
        case "netbanking":
            console.log("Bank: " + payment.bankName);
            break;
    }
}

processPayment({ method: "upi", upiId: "rahul@paytm" }, 1499);
// Processing ₹1499 via UPI
// UPI ID: rahul@paytm

processPayment({ method: "credit", cardNumber: "4111111111111234", expiryDate: "12/27", cvv: "123" }, 2999);
// Processing ₹2999 via CREDIT
// Card ending: 1234

Leave a Comment