Writing Custom Type Predicates: Using the is Syntax Correctly
AI generated
type
TypeScript
Writing Custom Type Predicates
Using the is syntax correctly

Custom type predicates let you tell the TypeScript compiler, through a function you write yourself, exactly what concrete type a value has after a check. Used correctly, they replace manual type assertions with real narrowing logic the compiler can actually follow.

10 min read TypeScript Type Guards

1. What a type predicate is and what the is syntax means

Built-in narrowing mechanisms like typeof, instanceof, or equality comparisons only work as long as the check sits directly in the same expression the compiler is analyzing. The moment that check gets extracted into its own function, that information is normally lost.

A type predicate solves this by giving the function a return type that is not boolean, but parameterName is Type. The compiler then treats a true return value as if the check had happened right at the call site.

The function itself still performs a perfectly ordinary boolean check at runtime, usually via typeof, in, or comparing individual properties. The is syntax is purely type-level information for the compiler, with no runtime effect of its own.


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

function printLength(value: unknown) {
  if (isString(value)) {
    console.log(value.length); // value is known to be string here
  }
}

2. Writing a simple custom type guard function

For custom classes or interfaces, a type predicate typically checks whether a specific property exists and has the expected type, usually combining the in operator with a type check on its value.

It matters that the parameter name inside the predicate exactly matches the function's parameter name: value is Dog only works for a parameter actually called value, not for one with a different name.

Type predicates can be applied to any function, whether it is defined as a named function, an arrow function, or a class method, as long as the signature carries the is return type.


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

function isDog(animal: Dog | Cat): animal is Dog {
  return "bark" in animal;
}

function makeSound(animal: Dog | Cat) {
  if (isDog(animal)) {
    animal.bark();
  } else {
    animal.meow();
  }
}

3. Type predicates for complex union types

For discriminated unions with a shared field like kind or type, a direct comparison inside an if is often enough, no custom function required. A dedicated predicate still pays off once the same check gets reused in several places in the code.

For unions without a shared discriminator field, a custom predicate is often the only practical solution, since the compiler otherwise cannot automatically infer membership from structure alone, especially with overlapping optional properties.

A single, well-named predicate like isSuccessResponse also makes such code more readable than a repeated, multi-line structural comparison written out at every call site.


interface SuccessResponse {
  status: "success";
  data: unknown;
}
interface ErrorResponse {
  status: "error";
  message: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;

function isSuccessResponse(res: ApiResponse): res is SuccessResponse {
  return res.status === "success";
}

4. Assertion functions as a complement: asserts x is T

Alongside type predicates, which return a boolean, TypeScript also has assertion functions using the syntax asserts value is Type, which throw an exception instead of returning a value if the condition is not met.

After calling such an assertion function, the compiler treats the rest of the code in the current block as if the condition were guaranteed to hold, with no enclosing if needed, because a failure would already have thrown an exception.

Assertion functions are especially well suited to preconditions at the start of a function, such as validating configuration values or API inputs, where an if-return pattern would needlessly nest the control flow.


function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error("Expected a string");
  }
}

function process(value: unknown) {
  assertIsString(value);
  console.log(value.toUpperCase()); // value is already known to be string here
}

5. Using type predicates in Array.filter

Without a type predicate, array.filter(item => item !== null) still returns an array with the original, nullable element type, since TypeScript does not automatically connect the filter condition to the resulting type.

When the filter function is instead written as a proper type predicate, TypeScript recognizes that every remaining element is guaranteed to match the narrower type and adjusts the return type of filter() accordingly.

This pattern is one of the most common practical uses for type predicates overall, since it makes filtering null or undefined out of arrays type-safe without needing an extra type assertion afterward.


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

const values: (string | undefined)[] = ["a", undefined, "b"];
const defined: string[] = values.filter(isDefined); // correctly typed

6. Writing generic type predicates

As shown in the previous isDefined example, type predicates can be combined with generic type parameters to write reusable guards that work for any concrete type instead of being tied to a single fixed type.

A common pattern is a generic predicate that checks whether a value is an instance of a given class, combined with a second generic parameter for the class constructor itself, to avoid repetition across many concrete classes.

Generic predicates are especially valuable in utility libraries imported by many projects, since a single, well-tested implementation replaces dozens of otherwise duplicated, project-specific guards.


function isInstanceOf<T>(
  value: unknown,
  ctor: new (...args: never[]) => T
): value is T {
  return value instanceof ctor;
}

class ValidationError extends Error {}

function handle(err: unknown) {
  if (isInstanceOf(err, ValidationError)) {
    console.log(err.message); // err is ValidationError here
  }
}

7. Pitfalls: unsafe type predicates as type lies

A type predicate is purely an assertion to the compiler, not a guarantee verified by the compiler. If the actual check inside the function body does not really match the claimed type, the result is a type lie the compiler cannot detect.

A classic example is a predicate that only checks whether a property exists, without checking its actual type, such as "id" in obj as supposed proof of obj is User, even though id can appear on completely unrelated object shapes.

Because a broken predicate throws no runtime error and instead silently propagates false type information to downstream code, every non-trivial predicate is worth a dedicated unit test covering both positive and negative cases.

8. Custom predicates vs. the in operator, typeof, and instanceof

For simple cases, primitive types, or known classes, typeof and instanceof right inside an if are entirely sufficient and need no custom predicate, since the compiler already understands these operators natively for narrowing.

The in operator also narrows automatically, but only based on a property's existence, not its type, which means it hits its limits with overlapping interfaces that share a property name but assign it different types.

A custom type predicate pays off mainly when the check logic is more complex than a single native operator can cover, or when the same check needs to be reused in multiple places instead of being rewritten every time.

9. Practical example: validating API responses with type predicates

When processing JSON responses from external APIs, TypeScript naturally has no static information about the actual shape of the data, which is why fetch().json() typically returns any.

A type predicate that actually checks the minimally expected structure at runtime, for instance through several typeof and in checks, combines genuine runtime validation with precise type information the compiler can use afterward.

For more complex schemas, a validation library like Zod is often worth reaching for in practice, since its .parse() or .safeParse() methods internally rely on the same principle as a hand-written type predicate, just generated from a declarative schema definition instead of coded by hand.


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

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

async function loadUser(url: string): Promise<ApiUser> {
  const data: unknown = await (await fetch(url)).json();
  if (!isApiUser(data)) {
    throw new Error("Unexpected API response");
  }
  return data;
}
Method Narrows automatically? Reusable? Typical use
typeof Yes, natively No, inline only Primitive types like string, number
instanceof Yes, natively No, inline only Class instances
in operator Yes, natively, existence only No, inline only Property existence in union types
Custom type predicate (is) Yes, via function call Yes, defined centrally Complex checks, Array.filter
Assertion function (asserts) Yes, no enclosing if needed Yes, defined centrally Preconditions, input validation

Mironsoft

TypeScript migration, type safety, and team onboarding

A JavaScript codebase without type safety, but no time for a full migration?

We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.

Migration Roadmap

Plan and execute a gradual JS-to-TS migration without big-bang risk.

Strict Mode Rollout

Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.

Team Onboarding

Bring developers up to speed on TypeScript best practices with workshops and reviews.

10. Summary

Custom Type Predicates

is syntax

parameterName is Type instead of boolean

Compile-time only

No automatic runtime verification

filter() pattern

Removes null/undefined type-safely

Testing required

Broken predicates are type lies

11. FAQ: Custom Type Predicates

1What is the difference between a type predicate and a regular boolean function?
A regular function only returns true or false with no further type information. A type predicate with is syntax additionally tells the compiler exactly what concrete type the checked value has when true.
2Does the parameter name in the predicate need to match exactly?
Yes. value is Dog only works if the checked function parameter is actually named value. With a different name, the compiler cannot make the connection.
3Does TypeScript verify that my type predicate is actually correct?
No. The compiler fully trusts the assertion in the return type without verifying the function body against that claim. A broken predicate gets accepted silently.
4When should I use an assertion function instead of a type predicate?
When a failed check justifies throwing an exception rather than returning a boolean, such as preconditions at the start of a function where continuing execution would not make sense otherwise.
5How does a type predicate help with Array.filter?
Without a predicate, the element type after filter() stays unchanged even if the condition excludes null or undefined. With a predicate like isDefined, TypeScript adjusts the resulting type correctly.
6Can a type predicate be generic?
Yes, generic predicates like isDefined can be reused for any concrete type instead of writing a nearly identical function for every single type.
7When is the built-in in operator enough instead of a custom predicate?
When the check relies purely on a property's existence and no additional type check on its value is needed. With overlapping property names of different types, in hits its limits.
8What happens if a type predicate is implemented incorrectly?
The compiler accepts the wrong type information without warning, so downstream code compiles based on an incorrect type but fails at runtime with unexpected values.
9Should I write a unit test for every type predicate?
For non-trivial predicates, yes, because a broken predicate produces no compile error and only surfaces at runtime, often far away from the actual root cause.
10Are type predicates suitable for validating API responses?
Yes, as long as the check logic in the function body actually matches the structure. For more complex schemas, a validation library like Zod is often worth using instead of a hand-written predicate.