HomeGuidesTypeScriptTypeScript Type Narrowing — typeof, instanceof, Custom Type Guards
🔷 TypeScript

TypeScript Type Narrowing and Type Guards

TypeScript narrows union types inside conditional blocks. Here's all the narrowing patterns and custom type guards.

Examifyr·2026·5 min read

Built-in narrowing

TypeScript narrows types automatically inside standard conditional checks.

function process(value: string | number | null) {
    // typeof narrows primitives
    if (typeof value === "string") {
        return value.toUpperCase();  // string
    }
    if (typeof value === "number") {
        return value.toFixed(2);     // number
    }
    // value is null here
    return "no value";
}

// Truthiness narrows null/undefined/falsy
function display(name: string | null) {
    if (name) {
        console.log(name.toUpperCase());  // string — null is excluded
    }
}

instanceof and in narrowing

instanceof narrows class instances. The in operator narrows object shapes.

class NetworkError extends Error {
    status: number;
    constructor(status: number) {
        super();
        this.status = status;
    }
}

function handleError(error: Error | NetworkError) {
    if (error instanceof NetworkError) {
        console.log(error.status);   // NetworkError properties available
    }
}

// in operator
type Cat = { meow(): void; purr(): void };
type Dog = { bark(): void; fetch(): void };

function interact(animal: Cat | Dog) {
    if ("meow" in animal) {
        animal.meow();   // animal is Cat
    } else {
        animal.bark();   // animal is Dog
    }
}

Custom type guard functions

Type guard functions use `value is Type` as their return type to help TypeScript narrow types.

// Type predicate: "value is User" tells TypeScript to narrow
function isUser(value: unknown): value is User {
    return (
        typeof value === "object" &&
        value !== null &&
        "id" in value &&
        "name" in value
    );
}

function processInput(data: unknown) {
    if (isUser(data)) {
        console.log(data.name);  // TypeScript knows data is User
    }
}

// Useful for validating API responses
async function getUser(id: string) {
    const data = await fetch(`/api/users/${id}`).then(r => r.json());
    if (!isUser(data)) throw new Error("Invalid user data");
    return data;  // type: User
}
Note: Custom type guards are essential for validating data at runtime boundaries (API responses, user input, localStorage).

The never type and exhaustive checks

The never type represents impossible values. It's used to ensure all union members are handled.

type Shape = "circle" | "square" | "triangle";

function getArea(shape: Shape): number {
    switch (shape) {
        case "circle":   return Math.PI * 5 ** 2;
        case "square":   return 5 ** 2;
        case "triangle": return (5 * 4) / 2;
        default:
            const exhaustiveCheck: never = shape;
            // If you add a new Shape without handling it here,
            // TypeScript will show an error at compile time
            throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
    }
}
Note: The exhaustive never check is a powerful pattern — TypeScript will give you a compile error if you add a new union member and forget to handle it.

Exam tip

Custom type guards (value is Type) are commonly asked in senior TypeScript interviews. Know that without them, TypeScript can't narrow types based on your own validation logic — the predicate syntax is what tells the compiler about the narrowing.

🎯

Think you're ready? Prove it.

Take the free TypeScript readiness test. Get a score, topic breakdown, and your exact weak areas.

Take the free TypeScript test →

Free · No sign-up · Instant results

← Previous
TypeScript Utility Types — Partial, Required, Pick, Omit, Record
← All TypeScript guides