Skip to content

Custom Type Guards

A type guard is a function that tells TypeScript:

“If this function returns true, then the value is of this specific type.”

This allows TypeScript to narrow types based on your own logic, not just built‑in operators like typeof or instanceof.


Writing value is Type Functions

A custom type guard is any function that returns a type predicate:

function isString(value: unknown): value is string {
  return typeof value === "string"
}

The magic is in the return type:

value is string

This tells TypeScript that inside an if (isString(value)) block, value should be treated as a string.

Example: Using the guard

function printUpper(value: unknown) {
  if (isString(value)) {
    console.log(value.toUpperCase()) // value: string
  } else {
    console.log("Not a string")
  }
}

Without the guard, TypeScript would not allow value.toUpperCase().


Type Guards for Objects

Type guards become extremely useful when checking object shapes.

Example: Guarding a User object

type 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 any).id === "number" &&
    typeof (value as any).name === "string"
  )
}

Using the guard

const data: unknown = JSON.parse('{"id":1,"name":"Alice"}')

if (isUser(data)) {
  console.log(data.name.toUpperCase()) // Safe
}

TypeScript now understands that data is a User inside the if.


Guarding External Data

External data is always unknown:

  • API responses
  • JSON files
  • user input
  • environment variables
  • query parameters
  • localStorage/sessionStorage

TypeScript cannot trust external data, so you must validate it.

Example: Guarding JSON from an API

type Product = {
  id: number
  title: string
  price: number
}

function isProduct(value: unknown): value is Product {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as any).id === "number" &&
    typeof (value as any).title === "string" &&
    typeof (value as any).price === "number"
  )
}

Using it with fetch

async function loadProduct() {
  const res = await fetch("/api/product")
  const data: unknown = await res.json()

  if (!isProduct(data)) {
    throw new Error("Invalid product data")
  }

  console.log(data.title) // Safe
}

This pattern prevents entire classes of runtime bugs.


Type Guards for Discriminated Unions

Custom guards can also narrow discriminated unions.

Example: Result type

type Success = { status: "success"; data: string }
type Failure = { status: "error"; error: Error }

type Result = Success | Failure

Custom guard

function isSuccess(result: Result): result is Success {
  return result.status === "success"
}

Using it

function handle(result: Result) {
  if (isSuccess(result)) {
    console.log(result.data)
  } else {
    console.error(result.error.message)
  }
}

This is cleaner and more reusable than repeating the same checks everywhere.


Type Guards for Arrays

You can validate arrays too.

Example: Guarding an array of numbers

function isNumberArray(value: unknown): value is number[] {
  return Array.isArray(value) && value.every(item => typeof item === "number")
}

Usage

const data: unknown = [1, 2, 3]

if (isNumberArray(data)) {
  const sum = data.reduce((a, b) => a + b, 0)
}

Putting It All Together

Here’s a realistic example combining multiple guard techniques:

type User = {
  id: number
  name: string
  roles: string[]
}

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as any).id === "number" &&
    typeof (value as any).name === "string" &&
    Array.isArray((value as any).roles) &&
    (value as any).roles.every((r: unknown) => typeof r === "string")
  )
}

async function loadUser() {
  const res = await fetch("/api/user")
  const data: unknown = await res.json()

  if (!isUser(data)) {
    throw new Error("Invalid user data")
  }

  console.log(data.roles.join(", "))
}

This is the exact pattern used in production systems to validate external data safely.


Summary

In this lesson, you learned how to create and use custom type guards:

1. Writing value is Type functions

  • Use type predicates to teach TypeScript new narrowing rules
  • Works for primitives, objects, arrays, and unions

2. Guarding external data

  • All external data should be treated as unknown
  • Validate shape and types before using
  • Prevents runtime crashes and invalid states

Custom type guards are one of the most powerful tools in TypeScript—they let you bring real‑world data safely into your type system.