TypeScript Type Guards — Narrow Types with Pattern Matching | TryJS

Learn TypeScript type guards and type narrowing with runnable examples. Switch-case pattern matching, typeof, instanceof, and custom predicate guards.

Overview

Type guards are expressions that narrow a value's type within a code branch. TypeScript's control-flow analysis recognizes typeof, instanceof, equality checks, the `in` operator, and user-defined type predicates, and uses them to refine the type as you go. This is how you take a union like string | number and end up in a branch where the compiler knows you have just a string.

How It Works

Built-in guards: typeof and instanceof

typeof x === 'string' narrows x to string in the true branch. x instanceof Error narrows to Error. Both are recognized by TypeScript's control-flow analysis with zero ceremony.

Discriminated unions + switch

Give each variant a literal tag property ('kind', 'type'), then switch on it. Inside each case, TypeScript narrows to that specific variant — including its extra fields.

User-defined type predicates

function isUser(x: unknown): x is User { ... } — the `x is User` return type tells TypeScript that the function's true return value implies the argument is a User. Lets you encapsulate complex narrowing logic.

The 'in' operator narrows by key

if ('length' in obj) narrows obj to the subset of the union that has a length property. Handy for duck-typed unions where there's no tag field.

Common Mistakes

When to Use It

Use discriminated unions for modelling 'one of N known shapes' — API responses, Redux actions, result types. Use type predicates when the narrowing logic is complex or needs to be reused. Use typeof/instanceof for everyday narrowing inside a single function.

Runnable Example

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

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rect":
      return shape.width * shape.height;
    case "triangle":
      return 0.5 * shape.base * shape.height;
    default: {
      // Exhaustiveness check: fails compilation if a case is missing
      const _exhaustive: never = shape;
      return _exhaustive;
    }
  }
}

console.log(area({ kind: "circle", radius: 5 }).toFixed(2));
console.log(area({ kind: "rect", width: 4, height: 6 }));
console.log(area({ kind: "triangle", base: 4, height: 3 }));

// User-defined type predicate
function isNonEmptyString(x: unknown): x is string {
  return typeof x === "string" && x.length > 0;
}

const values: unknown[] = ["hello", "", 42, null];
const strings = values.filter(isNonEmptyString);
console.log(strings); // inferred as string[]

Open this example in the TryJS playground to edit and run the code instantly in your browser — no signup needed.

Frequently Asked Questions

What is a type guard in TypeScript?

A type guard is an expression whose truthiness narrows a value's type in the branch it guards. Examples: typeof x === 'string', x instanceof Error, user-defined predicates like isUser(x).

What is the difference between type guards and type assertions?

A type guard narrows the type through runtime logic that TypeScript can verify. A type assertion (x as T) just tells the compiler 'trust me, it's a T' — no runtime check. Guards are safer; assertions are escape hatches.

What is a user-defined type predicate?

A function returning `x is T`. TypeScript uses its return value to narrow the argument's type. Example: function isString(x: unknown): x is string { return typeof x === 'string' }.

How do I enforce exhaustive switch on a union?

Add a default case that assigns the value to a variable of type never. If any case is missing, the assignment fails to compile. const _exhaustive: never = value.