Skip to main content
Technology & EngineeringTypescript Patterns130 lines

Type Guards

Narrow types at runtime with user-defined type predicates and assertion functions.

Quick Summary11 lines
You are an expert in Type Guards for writing type-safe TypeScript.

## Key Points

- Prefer `is` predicates over bare boolean returns so the compiler narrows the type automatically in calling code.
- Use assertion functions (`asserts x is T`) for fail-fast validation at function boundaries where you want to throw on invalid data.
- Combine `isDefined` filter guards with `Array.filter` to cleanly remove nulls from arrays while preserving the element type.
- A type predicate that lies — if the runtime check is incorrect the compiler trusts you, leading to unsound narrowing and runtime errors.
- Forgetting that assertion functions must throw (not return false) when the condition fails; returning without throwing leaves the type unnarrowed.
skilldb get typescript-patterns-skills/Type GuardsFull skill: 130 lines
Paste into your CLAUDE.md or agent config

Type Guards — TypeScript Patterns

You are an expert in Type Guards for writing type-safe TypeScript.

Overview

Type guards are runtime checks that narrow a variable's type within a conditional block. TypeScript recognizes built-in narrowing (typeof, instanceof, in, equality checks) and also supports user-defined type predicates (x is T) and assertion functions (asserts x is T). Use type guards whenever you receive data of an uncertain type — API responses, user input, union members — and need to work with it safely.

Core Philosophy

Type guards bridge the gap between TypeScript's static type system and JavaScript's dynamic runtime. The compiler can track types through control flow, but only when you give it enough information. A type guard is a runtime check that the compiler can understand and use to narrow a variable's type in subsequent code, making it safe to access properties that only exist on the narrowed type.

The is predicate and asserts function are the two mechanisms that let you extend the compiler's built-in narrowing with custom logic. A predicate function returns a boolean and tells the compiler "if this returns true, the argument is of type T." An assertion function tells the compiler "if this returns at all (without throwing), the argument is of type T." Both transfer your domain knowledge about data shapes into the type system so that downstream code does not need to repeat the same checks.

The trust relationship is the critical concept to internalize. When you write value is User, the compiler believes you unconditionally. If the runtime check is wrong — if it returns true for an object that is not actually a User — every downstream access based on that narrowing is unsound. Type guards must be correct, thorough, and tested, because they are the foundation on which the rest of the type-safe code stands.

Core Concepts

Built-in narrowing:

function process(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase());  // narrowed to string
  } else {
    console.log(value.toFixed(2));     // narrowed to number
  }
}

User-defined type predicate:

interface Fish { swim(): void }
interface Bird { fly(): void }

function isFish(animal: Fish | Bird): animal is Fish {
  return (animal as Fish).swim !== undefined;
}

Assertion function:

function assertDefined<T>(value: T | undefined, msg?: string): asserts value is T {
  if (value === undefined) {
    throw new Error(msg ?? "Value is undefined");
  }
}

function handle(input: string | undefined) {
  assertDefined(input);
  console.log(input.toUpperCase());  // narrowed to string
}

Implementation Patterns

Guarding discriminated union members:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rect"; width: number; height: number };

function isCircle(s: Shape): s is Extract<Shape, { kind: "circle" }> {
  return s.kind === "circle";
}

Generic type guard for nullable values:

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

const items = [1, null, 2, undefined, 3];
const defined: number[] = items.filter(isDefined);

Validating unknown API data:

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

function isUser(data: unknown): data is User {
  if (typeof data !== "object" || data === null) return false;
  const obj = data as Record<string, unknown>;
  return (
    typeof obj.id === "string" &&
    typeof obj.name === "string" &&
    typeof obj.email === "string"
  );
}

Best Practices

  • Prefer is predicates over bare boolean returns so the compiler narrows the type automatically in calling code.
  • Use assertion functions (asserts x is T) for fail-fast validation at function boundaries where you want to throw on invalid data.
  • Combine isDefined filter guards with Array.filter to cleanly remove nulls from arrays while preserving the element type.

Common Pitfalls

  • A type predicate that lies — if the runtime check is incorrect the compiler trusts you, leading to unsound narrowing and runtime errors.
  • Forgetting that assertion functions must throw (not return false) when the condition fails; returning without throwing leaves the type unnarrowed.

Anti-Patterns

Writing type predicates that check fewer fields than the type declares. If User has id, name, and email, but isUser only checks for id, then objects with an id but no name or email pass the guard and cause runtime errors when those fields are accessed. Validate every required field.

Using as casts inside type guards instead of proper runtime checks. A guard that does return (value as User).id !== undefined is checking a cast, not a runtime value. If value is null or not an object, this throws. Use typeof and in checks before accessing properties.

Creating type guards that mutate their arguments. A type guard should be a pure check — it observes the value and returns a boolean. If the guard transforms, normalizes, or coerces the value as a side effect, the narrowed type may not match the actual runtime value, breaking type safety.

Duplicating guard logic instead of composing guards. If isAdmin and isUser share validation logic, extract the shared checks into a base guard and compose them. Duplicated guards drift apart over time as one gets updated and the other does not.

Using assertion functions in hot paths where throwing is expensive. Assertion functions throw on failure, which means constructing an Error with a stack trace. In performance-sensitive loops, prefer predicate guards that return false and let the caller decide how to handle the failure without the cost of exception creation.

Install this skill directly: skilldb add typescript-patterns-skills

Get CLI access →