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.
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.
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.
// 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.
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:
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:
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:
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.
// 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:
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:
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:
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:
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:
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.
function isStringBad(value: unknown): boolean {
return typeof value === "string";
}
// No narrowing — return type is boolean, not value is stringMismatching the parameter name in the predicate. The name in the predicate must match one of the function's actual parameter names exactly.
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:
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
Key Insights
- A custom type guard returns
parameterName is Typeand 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.
Frequently Asked Questions
What is a custom type guard in TypeScript?
How do I write a type predicate?
Can I use custom type guards with array .filter()?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.