C.W.K.
Stream
Lesson 04 of 05 · published

Type Predicates: `is` Functions

~11 min · narrowing, type-predicates, type-guards, user-defined

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"When typeof, instanceof, and in aren't enough, write your own narrowing function."

The form of a type predicate

A type predicate is a function with the return type annotation arg is T. The signature looks like function isUser(x: unknown): x is User. The function's body returns a boolean as usual. The magic is in the return type: when the function returns true, the caller learns that arg is type T.

From the caller's perspective, calling the predicate is just like a typeof or instanceof check — the value gets narrowed inside the true branch.

What goes inside

The body of the predicate runs at runtime. It does whatever check is necessary to verify the type: property existence, value types, structural matching, even calling other predicates. The compiler trusts the predicate — if it returns true, the narrowing happens. (This means buggy predicates lie to the compiler; structure the body carefully.)

For unknown inputs from external sources (JSON, user input, third-party APIs), the predicate is your validation. Tools like Zod, Valibot, and io-ts wrap this pattern with parser-generators that produce type-safe predicates.

When to write your own

  • Validating untyped data (parsed JSON, dynamic fetches).
  • Checking object shape for unions that don't have a clean discriminator.
  • Wrapping checks that are too complex for a single typeof/instanceof.
The compiler trusts your predicate. If your predicate returns true for an object that doesn't actually match T, the type system is now lying. Write predicates with the same care as a JSON parser — they're the boundary between unsafe input and safe code.

Code

A custom isUser predicate·typescript
interface User { name: string; email: string }

// A type predicate — note the return type 'x is User'.
function isUser(x: unknown): x is User {
  return (
    typeof x === 'object' && x !== null &&
    'name' in x && typeof (x as any).name === 'string' &&
    'email' in x && typeof (x as any).email === 'string'
  );
}

// Use it like any other narrowing check.
function handle(input: unknown) {
  if (isUser(input)) {
    // input narrowed to User here.
    return `Hello, ${input.name}`;
  }
  return 'Invalid input';
}

// Without the predicate, you'd narrow with property checks inline — verbose,
// not reusable, and you'd lose the User-typed access at the end.
Composable predicates and Array.isArray·typescript
// Array-specific predicate — Array.isArray IS a built-in type predicate.
function processItems(x: unknown) {
  if (Array.isArray(x)) {
    // x narrowed to any[] (built-in predicate)
    return x.length;
  }
  return 0;
}

// You can compose predicates for nested structures.
function isStringArray(x: unknown): x is string[] {
  return Array.isArray(x) && x.every((v) => typeof v === 'string');
}

function collect(input: unknown) {
  if (isStringArray(input)) {
    // input: string[]
    return input.join(', ');
  }
  return '';
}

External links

Exercise

Write a predicate isApiResponse(x: unknown): x is { status: number; data: unknown }. Use property-existence and type checks for status and data. Then write a function that consumes the result of a fetch().then(r => r.json()) and uses the predicate to safely access data.
Hint
The body needs to verify x is an object, not null, has status as a number, and has data (you can leave data as unknown since the predicate only narrows the outer shape). Once narrowed, you can drill into data using more predicates.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.