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.
Table of Contents
- 1. What a type predicate is and what the is syntax means
- 2. Writing a simple custom type guard function
- 3. Type predicates for complex union types
- 4. Assertion functions as a complement: asserts x is T
- 5. Using type predicates in Array.filter
- 6. Writing generic type predicates
- 7. Pitfalls: unsafe type predicates as type lies
- 8. Custom predicates vs. the in operator, typeof, and instanceof
- 9. Practical example: validating API responses with type predicates
- 10. Summary
- 11. FAQ
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