Skip to content
TypeScript ts narrowing 3 min read

Custom Type Guards

The built-in narrowing operators — typeof, instanceof, in, equality, and truthiness — cover many cases, but sometimes you need to express “this value is a Customer” with your own logic. A user-defined type guard is a function whose return type is a type predicate of the form parameter is Type. When the function returns true, TypeScript narrows the argument to that type at the call site. This page covers writing predicates, validating unknown data, guarding array elements, and the safety responsibilities a guard carries.

The type predicate signature

A type guard is an ordinary function that returns a boolean, but its return type is annotated as param is Type. TypeScript then uses the runtime result to narrow.

interface Cat {
  meow(): void;
}
interface Dog {
  bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return "meow" in animal;
}

function speak(animal: Cat | Dog): void {
  if (isCat(animal)) {
    // animal: Cat
    animal.meow();
  } else {
    // animal: Dog
    animal.bark();
  }
}

The predicate animal is Cat tells the compiler that a true result means Cat and a false result means it is not Cat (so Dog in this union).

Guarding unknown values

Type guards are essential at trust boundaries — parsing JSON, reading API responses, or handling user input typed as unknown. The guard performs the runtime validation and hands back a precise type.

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

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof (value as Record<string, unknown>).id === "number" &&
    "name" in value &&
    typeof (value as Record<string, unknown>).name === "string"
  );
}

const data: unknown = JSON.parse('{"id":1,"name":"Ada"}');
if (isUser(data)) {
  // data: User
  console.log(data.name.toUpperCase());
}

Each field is checked at runtime so the User narrowing is actually justified.

The guard’s safety contract

A type predicate is an assertion that the compiler trusts. If the runtime check does not truly verify the type, you create an unsound narrowing that fails later — the compiler cannot catch a lying guard.

// ❌ Unsafe: claims User but never checks the shape
function badGuard(value: unknown): value is User {
  return true; // compiles, but lies to the type system
}

A user-defined type guard puts correctness in your hands. The compiler will not verify that your runtime logic matches the predicate, so the check must genuinely confirm the type.

Narrowing arrays with a guard

A guard combined with Array.prototype.filter produces a correctly narrowed array — something a plain truthiness filter cannot do.

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

const raw: (string | null)[] = ["a", null, "b", null];

// Without the guard, filtered would still be (string | null)[]
const filtered: string[] = raw.filter(isDefined);

The generic value is T predicate strips null | undefined, so filter returns string[] rather than (string | null)[].

Built-in guards vs custom guards

ApproachUse when
typeofnarrowing primitive unions
instanceofnarrowing class instances
indistinguishing object shapes you do not own
custom x is Tvalidating unknown, complex shapes, reusable checks
assertion functionsthrowing instead of branching (see below)

For shapes that need throwing rather than a boolean branch, see assertion functions. For schema-heavy validation, libraries like Zod generate guards for you.

Best Practices

  • Annotate guards as param is Type and make the runtime check genuinely verify that type.
  • Use guards at every trust boundary: JSON parsing, network responses, and unknown inputs.
  • Keep guards small, pure, and reusable; name them isX for readability.
  • Write generic guards like isDefined<T> to narrow arrays via filter.
  • Never return a hardcoded true — an unsound predicate defeats the type system silently.
  • Prefer built-in narrowing first; reach for a custom guard only when operators cannot express the check.
Last updated June 29, 2026
Was this helpful?