Articles

TypeScript Type Guards, Explained

Type guards are functions and checks that narrow a TypeScript union to a specific type at runtime. How typeof, instanceof, in, and custom guards work.

Takina Takina · · 5 min read
Dark-themed code editor showing TypeScript syntax

A type guard is a runtime check that tells TypeScript’s compiler which specific type a value has within a union type, so it lets you use type-specific properties and methods without a cast. TypeScript’s type system disappears at runtime — every .ts file compiles down to plain JavaScript with no type information left — so type guards are the bridge: an ordinary JavaScript check (typeof, instanceof, a property test) that the compiler recognizes and uses to narrow the type for the rest of that code branch.

Why narrowing is necessary

Consider a function that accepts a string | number:

function format(value: string | number) {
  return value.toUpperCase(); // Error: toUpperCase does not exist on type 'number'
}

TypeScript won’t let you call toUpperCase() because value might be a number at runtime. You have to narrow the union to string first:

function format(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // OK — value is `string` here
  }
  return value.toFixed(2); // OK — value is `number` here
}

That typeof value === "string" check is a type guard. Inside the if block, TypeScript narrows value’s type to string; in the else branch (or after an early return), it narrows to number. This is called control flow analysis, and it’s what makes TypeScript feel smart about types without any extra annotations.

The built-in guards

TypeScript recognizes several JavaScript operators as narrowing checks automatically:

  • typeof — narrows primitives: string, number, boolean, bigint, symbol, undefined, function, object.
  • instanceof — narrows class instances: if (error instanceof TypeError) narrows error to TypeError inside the block.
  • in — narrows by property presence, useful for object shapes that aren’t classes: if ("bark" in animal) narrows animal to whichever union member has a bark property.
  • Equality checksvalue === null, value !== undefined, or comparing a discriminant field like shape.kind === "circle" all narrow.
  • Array.isArray — narrows to an array type, since typeof can’t distinguish arrays from objects.
function describe(pet: Dog | Cat) {
  if ("bark" in pet) {
    pet.bark(); // narrowed to Dog
  } else {
    pet.meow(); // narrowed to Cat
  }
}

Writing a custom type guard

For shapes the built-ins can’t express — validating unknown data from an API response, for instance — you write a function whose return type is a type predicate: value is SomeType.

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

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "email" in value
  );
}

function handle(data: unknown) {
  if (isUser(data)) {
    console.log(data.email); // narrowed to User
  }
}

The value is User return annotation is what makes this a type guard rather than an ordinary boolean function — without it, TypeScript would only know the function returns boolean and wouldn’t narrow anything at the call site. This pattern is the standard way to validate data of type unknown coming from JSON.parse, a fetch response, or any other untrusted boundary.

Discriminated unions: the common case

The most common real-world use of type guards is narrowing a discriminated union — a set of object types that share a literal “tag” field.

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

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // narrowed to circle
    case "rectangle":
      return shape.width * shape.height; // narrowed to rectangle
  }
}

Here the guard is just shape.kind === "circle", generated implicitly by the switch. No custom predicate function is needed because the discriminant field does the narrowing for you — this pattern is worth reaching for any time a union has more than two members.

Type guards vs type assertions

It’s worth distinguishing type guards from as assertions, since both are ways of telling the compiler something about a type — but only one of them checks anything:

Type guardType assertion (as)
Checked at runtimeYesNo
Compiler verifies itYes, via control flow analysisNo — you’re overriding the compiler
Safe if wrongYes — the branch just doesn’t runNo — silent type errors at runtime
Typical useNarrowing a union, validating unknown dataTelling the compiler something it can’t infer, when you’re certain

An assertion like data as User compiles away to nothing and performs no check — if data isn’t actually a User, you get a runtime error somewhere downstream instead of a caught type error. A type guard, by contrast, is a real if check that fails safely.

Where guards fit in a typical codebase

Type guards show up constantly in code that deals with data whose shape isn’t fully known until runtime: parsing JSON from a REST API, handling different event types in a reducer, or working with generics where a function accepts more than one possible input shape. They’re also the mechanism behind exhaustiveness checking — a switch over a discriminated union that handles every case, verified by an assertNever in the default branch. If you’re new to TypeScript’s type system generally, our getting started guide covers narrowing alongside the rest of the basics.

The takeaway

A type guard is any check — typeof, instanceof, in, a discriminant comparison, or a custom value is T predicate — that TypeScript’s control flow analysis recognizes and uses to narrow a union to a specific member. Built-in guards cover primitives and class instances; custom predicate functions cover validating unknown data at runtime boundaries; discriminated unions with a literal tag field cover almost everything else. Reach for a guard, not an as assertion, any time the compiler is complaining about a type you can actually check.

Takina Takina · · 4 min read

TypeScript Abstract Classes, Explained

Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.

#TypeScript #JavaScript #Web Development
Takina Takina · · 3 min read

TypeScript's never Type, Explained

never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.

#TypeScript #JavaScript #Web Development
Takina Takina · · 4 min read

TypeScript readonly Modifiers Explained

TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.

#TypeScript #JavaScript #Web Development