DevAcademy
LearnTypeScriptCustom Type Guards
AdvancedTypeScript

Custom Type Guards

Learn how to write your own type predicate functions to teach TypeScript custom narrowing logic.

Reading Time

16 min

Lesson

Lesson 25 of 30

When Built-In Narrowing Isn’t Enough

typeof, instanceof, and in cover a lot of narrowing scenarios, but sometimes checking a type requires custom logic — validating shape of unknown JSON data, for example. Custom type guards let you write that logic once and reuse it everywhere.

The is Type Predicate

A function whose return type is written as parameterName is Type tells TypeScript: "if this function returns true, treat the parameter as this specific type from now on."

A Basic Type Guard

interface Cat {
  meow(): void;
}

interface Dog {
  bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return (animal as Cat).meow !== undefined;
}

function makeSound(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow(); // narrowed to Cat
  } else {
    animal.bark(); // narrowed to Dog
  }
}

Validating Unknown Data

Type guards are especially useful for safely validating data of type unknown — like a parsed API response — before trusting its shape.

A Type Guard for Unknown Data

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

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

function processData(data: unknown) {
  if (isUser(data)) {
    console.log(data.name); // safely narrowed to User
  }
}

Type Guards as Array Filters

A type guard used inside Array.prototype.filter() also narrows the resulting array’s element type — a common pattern for removing null or undefined values from an array.

Filtering Out null with a Type Guard

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

const values: (string | null)[] = ["a", null, "b", null];
const cleaned = values.filter(isDefined); // typed as string[], not (string | null)[]

A Type Guard is a Promise, Not a Guarantee

TypeScript trusts the return type you declare, but doesn’t verify your guard’s logic is actually correct — an incorrect implementation compiles fine but narrows to the wrong type, reintroducing the exact bugs type guards are meant to prevent.

Best Practice

Write a type guard whenever you find yourself repeating the same manual narrowing check (typeof, in, property checks) in more than one place — extracting it keeps validation logic in a single, testable location.

Interview Questions

Quick Quiz

1. What does a function return type like value is User indicate?

2. Why are custom type guards useful for validating unknown data?

3. Does TypeScript verify that a type guard’s implementation is actually correct?