Custom Type Guards in TypeScript

Custom type guards let you define your own narrowing logic with the parameterName is Type return syntax. Learn how to write them, filter arrays, and compose guards for complex types.

7 min read

A custom type guard is a function that tells TypeScript to narrow a value to a specific type. Instead of returning a plain boolean, it returns a special type predicate: parameterName is TargetType. When the function returns true, TypeScript narrows the argument to that target type in the calling code.

typescripttypescript
function isString(value: unknown): value is string {
  return typeof value === "string";
}
 
function print(value: unknown) {
  if (isString(value)) {
    console.log(value.toUpperCase()); // value is string
  } else {
    console.log("Not a string");
  }
}

The return type value is string is the type predicate. It is not just documentation, it is what TypeScript uses to narrow the value in the if branch.

Why Custom Type Guards Exist

Built-in type guards like typeof, instanceof, and in cover common cases, but each one only understands a narrow slice of JavaScript's runtime behavior. Custom type guards cover everything else: checking object shapes with several fields, validating data that arrived from outside the program, and filtering arrays down to one member of a union.

typescripttypescript
// typeof cannot distinguish these
type Success = { ok: true; data: string };
type Failure = { ok: false; error: string };
type Result = Success | Failure;
 
// Custom type guard handles it
function isSuccess(result: Result): result is Success {
  return result.ok === true;
}

The logic inside the guard checks result.ok. TypeScript narrows based on the declared predicate type, not the implementation, so you are responsible for writing a check that actually matches what the predicate promises.

The Type Predicate Syntax

A type predicate has the form parameterName is Type:

  • parameterName must match one of the function's parameter names.
  • Type can be any valid TypeScript type: a primitive, an interface, a union member, a literal type.
typescripttypescript
function isNumber(x: unknown): x is number {
  return typeof x === "number";
}
 
function isDate(x: unknown): x is Date {
  return x instanceof Date;
}
 
function isNonEmptyString(x: unknown): x is string {
  return typeof x === "string" && x.length > 0;
}

Each guard narrows to a specific type, and the parameter type is usually unknown since the whole point is to check a value whose type is not yet known. The function name often starts with "is" by convention, which makes call sites read naturally inside an if statement.

Narrowing Union Members

Custom type guards shine when you need to pick one member out of a union of plain object types, especially when the check requires more logic than a single property lookup can express:

typescripttypescript
type Fish = { swim: () => void; gills: boolean };
type Bird = { fly: () => void; wings: boolean };
 
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

Calling the guard inside an if statement narrows the parameter for the rest of that branch, unlocking the field that only exists on Fish:

typescripttypescript
function care(pet: Fish | Bird) {
  if (isFish(pet)) {
    console.log("Has gills:", pet.gills); // pet is Fish
  } else {
    console.log("Has wings:", pet.wings); // pet is Bird
  }
}

Inside the guard, you temporarily assert pet as Fish to check for the swim property. The cast only affects the implementation, since the return type predicate is what drives narrowing at the call site.

Filtering Arrays with Type Guards

Pass a type guard to the array filter method and TypeScript narrows the resulting array:

typescripttypescript
const mixed: (string | number)[] = ["hello", 42, "world", 99];
 
const strings = mixed.filter(isString);
// strings is string[] — TypeScript knows the result
 
const numbers = mixed.filter((x): x is number => typeof x === "number");
// numbers is number[]

Without the type predicate, filter returns the same union array type. The predicate tells TypeScript the filter actually removes certain types.

typescripttypescript
// Without type predicate — result type unchanged
const bad = mixed.filter(x => typeof x === "string");
// bad is still (string | number)[]

This is one of the most practical uses of custom type guards, since filtering by type is common when working with mixed arrays and TypeScript cannot infer the narrower type on its own.

Composing Type Guards

A type guard can call another type guard from inside its own check, building a more specific guard out of a simpler one instead of repeating the same logic:

typescripttypescript
type ApiResponse =
  | { ok: true; data: { id: string }[] }
  | { ok: false; error: string };
 
function isSuccess(response: ApiResponse): response is { ok: true; data: { id: string }[] } {
  return response.ok === true;
}
 
function hasNonEmptyData(response: ApiResponse): response is { ok: true; data: { id: string }[] } {
  return isSuccess(response) && response.data.length > 0;
}

The second guard reuses the first one instead of repeating the ok === true check. TypeScript still narrows correctly at the call site, because the outer function's own return type is what matters, not how it arrived at that result:

typescripttypescript
function fetchUsers(): ApiResponse {
  return { ok: true, data: [{ id: "1" }] };
}
 
const response = fetchUsers();
if (hasNonEmptyData(response)) {
  response.data.forEach(user => console.log(user.id)); // Safe
}

Type Guards with Classes

A class method can narrow this using a this is Type predicate instead of checking a parameter. This is useful when a base class has several optional fields, and a method should confirm which ones are actually present:

typescripttypescript
class Animal {
  constructor(public name: string, public trainedTricks?: string[]) {}
 
  canPerform(this: Animal): this is Animal & { trainedTricks: string[] } {
    return this.trainedTricks !== undefined;
  }
}

Calling the method inside an if statement narrows this for the rest of that branch, the same way a regular type guard narrows a parameter:

typescripttypescript
const pet = new Animal("Rex", ["sit", "stay"]);
 
if (pet.canPerform()) {
  console.log(pet.trainedTricks.join(", ")); // trainedTricks is string[] here
}

Important: The Guard is Not Validated

TypeScript trusts the predicate return type completely. It does not inspect the function body to verify the check is actually correct, unlike typeof or instanceof, which the compiler understands natively and cannot be faked. A wrong guard produces wrong narrowing, and the mistake only shows up once the code actually runs:

typescripttypescript
function badGuard(x: unknown): x is string {
  return true; // Always returns true, but TypeScript still trusts it
}
 
function example(x: unknown) {
  if (badGuard(x)) {
    console.log(x.toUpperCase()); // Compiles, but may crash at runtime
  }
}

If the guard is wrong and the value is actually a number, calling a string method on it crashes at runtime. The compile-time safety is only as good as your implementation.

Common Mistakes

Returning a plain boolean instead of a type predicate. If the return type is boolean, TypeScript treats the function like any other boolean check and does not narrow anything at the call site, even if the runtime logic is identical to a real type guard.

typescripttypescript
function isStringBad(value: unknown): boolean {
  return typeof value === "string";
}
// No narrowing — return type is boolean, not value is string

Mismatching the parameter name in the predicate. The name in the predicate must match one of the function's actual parameter names exactly.

typescripttypescript
function check(data: unknown): value is string {
  return typeof data === "string";
}

The parameter is named data, but the predicate refers to value, which does not exist on this function. TypeScript rejects the mismatch:

texttext
Cannot find name 'value'.

Using a type guard when a discriminated union is simpler. If you control the types, a shared discriminant property is usually cleaner than a custom guard, since the compiler can check exhaustiveness for you. Reach for a custom guard mainly when you receive data you do not control, such as an API response or a value read from storage.

Writing overly broad type guards. A guard that always returns true, or that checks too little of the value's actual shape, defeats the purpose of narrowing. Write a specific check that reliably distinguishes the target type from every other member of the union.

Custom type guards pair well with discriminated unions for modeling state, and with exhaustive checks to ensure every variant is handled.

Rune AI

Rune AI

Key Insights

  • A custom type guard returns parameterName is Type and acts as a compile-time narrowing signal.
  • The function body must perform the actual runtime check; TypeScript trusts the predicate return type.
  • Pass type guards to .filter() to produce a narrowed array type automatically.
  • Type guards compose: you can call one guard inside another.
  • The implementation is not validated. If your check is wrong, narrowing will be wrong too.
RunePowered by Rune AI

Frequently Asked Questions

What is a custom type guard in TypeScript?

A custom type guard is a function whose return type uses the syntax `parameterName is Type`. When the function returns true, TypeScript narrows the argument to that type in the calling scope. It gives you full control over narrowing logic.

How do I write a type predicate?

Write a function that returns `parameterName is Type` instead of `boolean`. The parameter name must match one of the function's parameters. Inside the function, return true when the value matches the type, false otherwise.

Can I use custom type guards with array .filter()?

Yes. Pass a type guard function to .filter() and TypeScript narrows the resulting array type. For example, arr.filter(isString) returns string[] instead of (string | number)[].

Conclusion

Custom type guards give you full control over narrowing logic. Write a function that returns value is TargetType, implement the runtime check, and TypeScript handles the rest. Use them to filter arrays, compose multiple checks, and handle complex type distinctions that typeof and instanceof cannot express.