Skip to content

TypeScript Type Guards: Making the Type System Work for You

October 30, 2024 · 1 min read

TypeScript's type system is powerful, but sometimes you need to convince the compiler that you know more about a value's type than it does. That's where type guards come in.

What Are Type Guards?

Type guards are expressions that perform runtime checks and inform TypeScript's type checker about a value's type in a specific scope.

The simplest type guard is typeof:

function processValue(value: string | number) {
    if (typeof value === "string") {
        // TypeScript knows value is a string here
        console.log(value.toUpperCase());
    } else {
        // TypeScript knows value is a number here
        console.log(value.toFixed(2));
    }
}

Custom Type Guards

For complex types, you can write custom type guard functions:

interface User {
    id: number;
    name: string;
    email: string;
}

interface Admin extends User {
    permissions: string[];
}

// Type predicate: 'user is Admin'
function isAdmin(user: User): user is Admin {
    return 'permissions' in user;
}

function handleUser(user: User) {
    if (isAdmin(user)) {
        // TypeScript knows user is Admin here
        console.log(`Admin with ${user.permissions.length} permissions`);
    } else {
        // Regular user
        console.log(`Regular user: ${user.name}`);
    }
}

Discriminated Unions

One of my favorite patterns is discriminated unions with type guards:

type Result<T, E> =
    | { success: true; value: T }
    | { success: false; error: E };

function processResult<T, E>(result: Result<T, E>): void {
    if (result.success) {
        // result.value is available
        console.log("Success:", result.value);
    } else {
        // result.error is available
        console.log("Error:", result.error);
    }
}

The success field acts as a discriminant—TypeScript uses it to narrow the type automatically.

Array Type Guards

Filtering arrays with type guards:

function isNotNull<T>(value: T | null): value is T {
    return value !== null;
}

const values: (string | null)[] = ["hello", null, "world", null];
const strings: string[] = values.filter(isNotNull);
// Type is string[], not (string | null)[]

Why This Matters

Type guards bridge the gap between runtime checks and compile-time types. They let you write code that's both type-safe and handles real-world data that doesn't always match your expectations.

The key is teaching the type system what you know to be true based on your runtime checks. When you do this well, TypeScript becomes a powerful ally in catching bugs before they reach production.

← Back to all posts