TypeScriptUser-Defined Type Guards (is)

User-Defined Type Guards (is)

TypeScript can automatically narrow types through if checks, typeof, instanceof, and in. But sometimes you need to teach the compiler about a custom check — one that TypeScript can't figure out on its own. That's where user-defined type guards come in.

A type guard is a function whose return type is a type predicate: paramName is Type. When such a function returns true, TypeScript narrows the parameter to that type.

The Problem Without Type Guards

Consider this helper function. It checks if a value is a string — but TypeScript doesn't know that, so the narrowing doesn't work:

TS
// Without a type predicate — TypeScript doesn't narrow
function isString(value: unknown): boolean {
  return typeof value === 'string'
}

function process(value: string | number) {
  if (isString(value)) {
    // TypeScript still thinks: value is string | number
    // value.toUpperCase() would be an error!
    console.log(value) // value is still string | number
  }
}
Writing a Type Predicate

Change the return type from boolean to paramName is Type. Now TypeScript narrows automatically when the function returns true.

TS
// With a type predicate — TypeScript narrows correctly
function isString(value: unknown): value is string {
  return typeof value === 'string'
}

function process(value: string | number) {
  if (isString(value)) {
    // TypeScript now knows: value is string
    console.log(value.toUpperCase()) // safe!
  } else {
    // TypeScript knows: value is number
    console.log(value.toFixed(2)) // safe!
  }
}
Note
The parameter name in the predicate (value is string) must match the parameter name in the function signature. TypeScript uses this to know which variable gets narrowed at the call site.
Type Guards for Interface Discrimination

Type predicates shine when distinguishing between interfaces that don't share a class hierarchy. This is the primary use case in real applications.

TS
interface Dog {
  breed: string
  bark(): void
}

interface Cat {
  indoor: boolean
  meow(): void
}

type Pet = Dog | Cat

// Check if the pet is a Dog
function isDog(pet: Pet): pet is Dog {
  return 'bark' in pet
}

// Check if the pet is a Cat
function isCat(pet: Pet): pet is Cat {
  return 'meow' in pet
}

function greet(pet: Pet) {
  if (isDog(pet)) {
    pet.bark() // TypeScript knows it's a Dog
    console.log(`Breed: ${pet.breed}`)
  } else if (isCat(pet)) {
    pet.meow() // TypeScript knows it's a Cat
    console.log(`Indoor: ${pet.indoor}`)
  }
}
Type Guards with Arrays

Type guards work beautifully with .filter(). Without them, filtering arrays of mixed types loses type information. With them, TypeScript correctly infers the filtered type.

TS
const items: (string | null | undefined)[] = ['hello', null, 'world', undefined, 'foo']

// Without type guard — TypeScript can't infer the filtered type
const bad = items.filter(x => x !== null && x !== undefined)
// type: (string | null | undefined)[] — still includes null/undefined in type!

// With type predicate — TypeScript knows the result is string[]
function isNonNull<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined
}

const strings = items.filter(isNonNull)
// type: string[] — correct!

console.log(strings) // ['hello', 'world', 'foo']
['hello', 'world', 'foo']
Validating Unknown Data (API Responses)

One of the most important real-world uses of type predicates is validating data that comes from external sources — JSON from an API, localStorage, user input, etc.

TS
interface User {
  id: number
  name: string
  email: string
}

function isUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data &&
    'email' in data &&
    typeof (data as any).id === 'number' &&
    typeof (data as any).name === 'string' &&
    typeof (data as any).email === 'string'
  )
}

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  const data: unknown = await res.json()

  if (!isUser(data)) {
    throw new Error('Invalid user data from API')
  }

  // TypeScript knows: data is User
  return data
}
Tip
For production apps, consider using a validation library like **Zod** or **io-ts** instead of hand-writing type guards. They generate type predicates automatically from schemas and produce better error messages.
Chaining Multiple Type Guards

TS
type StringOrNumberOrBool = string | number | boolean

function isNumber(val: unknown): val is number {
  return typeof val === 'number'
}

function isString(val: unknown): val is string {
  return typeof val === 'string'
}

function formatValue(val: StringOrNumberOrBool): string {
  if (isNumber(val)) {
    return val.toFixed(2)         // val: number
  }
  if (isString(val)) {
    return val.trim().toUpperCase() // val: string
  }
  // val: boolean (only possibility left)
  return val ? 'Yes' : 'No'
}
Common Mistakes

Mistake

Problem

Correct approach

Lying in a type guard

Returning true for wrong type causes runtime crashes

Always validate all required properties

Using any in guard body carelessly

Bypasses type checking inside the guard

Cast to Record or use in checks

Forgetting null check

"obj is null" still passes "object" typeof check

Check !== null before property access

Asserting nested types shallowly

Only checks outer shape, misses nested structure

Validate all required nested properties

TypeScript 5.5: Inferred Type Predicates

TypeScript 5.5 introduced inferred type predicates — in simple cases, TypeScript can automatically infer the predicate type from the function body.

TS
// TypeScript 5.5+ — predicate inferred automatically
function isDefinedString(x: string | undefined) {
  return x !== undefined // return type inferred as: x is string
}

const values = ['a', undefined, 'b', undefined, 'c']
const defined = values.filter(isDefinedString)
// type: string[] — TypeScript 5.5+ infers this without explicit predicate
Practical: Redux Action Type Guards

TS
// Common in Redux or event-based architectures
interface LoadAction {
  type: 'LOAD'
  url: string
}

interface SuccessAction {
  type: 'SUCCESS'
  data: unknown[]
}

interface ErrorAction {
  type: 'ERROR'
  message: string
}

type AppAction = LoadAction | SuccessAction | ErrorAction

function isLoadAction(action: AppAction): action is LoadAction {
  return action.type === 'LOAD'
}

function isErrorAction(action: AppAction): action is ErrorAction {
  return action.type === 'ERROR'
}

function handleAction(action: AppAction) {
  if (isLoadAction(action)) {
    console.log(`Loading from ${action.url}`)
  } else if (isErrorAction(action)) {
    console.log(`Error: ${action.message}`)
  } else {
    console.log(`Got ${action.data.length} items`)
  }
}
Success
User-defined type guards bridge the gap between runtime validation and compile-time type safety. They're the foundation of every robust TypeScript codebase that deals with external data, union discrimination, or array filtering.