Writing your own type guards
The built-in narrowing tools — typeof, in, instanceof, the discriminant check — cover most cases.
But sometimes you need to narrow based on a custom condition the compiler cannot follow on its own:
"is this value a valid User?" You teach TypeScript to narrow by writing a type guard — a function
whose return type says "if this returns true, the argument is this type". This lesson is writing
them, and the related assertion functions.
The problem type guards solve
Suppose you have an unknown value (from an API, say) and a function that checks whether it is a
User:
type User = { name: string; email: string };
function isUser(value: unknown): boolean {
return (
typeof value === "object" && value !== null &&
"name" in value && "email" in value
);
}
const data: unknown = JSON.parse(input);
if (isUser(data)) {
data.name; // error! data is still 'unknown' — the boolean return told TypeScript nothing
}
The check is correct at runtime, but isUser returns a plain boolean, which tells TypeScript
nothing about data's type. Inside the if, data is still unknown, so data.name errors. The
compiler does not know that "isUser returned true" means "data is a User" — you have to tell
it.
The type predicate — value is Type
You teach TypeScript by giving the function a special return type: value is Type (a "type
predicate") instead of boolean:
function isUser(value: unknown): value is User {
return (
typeof value === "object" && value !== null &&
"name" in value && "email" in value &&
typeof (value as User).name === "string" &&
typeof (value as User).email === "string"
);
}
const data: unknown = JSON.parse(input);
if (isUser(data)) {
data.name.toUpperCase(); // fine! data is narrowed to User inside the guard
}
The return type value is User says: "this function returns a boolean, and when it returns true, the
argument is a User." Now if (isUser(data)) narrows data to User inside the branch — exactly like
typeof does for primitives, but for your custom check. A type guard is a function that returns a
type predicate, turning a runtime check into a compile-time narrowing.
Note the responsibility this carries: TypeScript trusts your predicate. If your runtime check is
wrong — you claim value is User but only checked name, not email — TypeScript believes you, and
the safety is only as good as your check. A type guard is a promise; make the runtime logic actually
verify what the predicate claims.
Guarding array elements — filter with a guard
A genuinely useful application: narrowing the elements of an array. filter with a type guard
narrows the resulting array's type:
const values: (string | null)[] = ["Pune", null, "Mumbai", null];
const strings = values.filter((v): v is string => v !== null);
// strings has type string[] — the nulls are removed AND the type reflects it
Without the : v is string predicate, filter would return (string | null)[] (TypeScript cannot
know your callback removed the nulls). With the predicate, filter returns string[]. This is the
typed version of "drop the nulls", and it is a common, elegant use of a type guard inline. (For this
specific null case, TypeScript's built-in filter overloads sometimes handle it, but the explicit
guard is reliable and clear.)
Assertion functions — narrow or throw
A cousin of the type guard is an assertion function: instead of returning a boolean, it throws if the condition fails, and narrows the type for all code after the call:
function assertIsUser(value: unknown): asserts value is User {
if (
typeof value !== "object" || value === null ||
!("name" in value) || !("email" in value)
) {
throw new Error("Not a valid User");
}
}
const data: unknown = JSON.parse(input);
assertIsUser(data);
data.name.toUpperCase(); // fine — after the assertion, data is User (or we threw)
The return type asserts value is User says: "if this function returns normally, the argument is a
User (otherwise it threw)." So after assertIsUser(data), TypeScript narrows data to User for the
rest of the scope — no if needed, because if the value were not a User, the function would have
thrown and execution would have stopped. This is the "validate at the boundary, then proceed with a
guaranteed type" pattern, and it reads very cleanly for input validation.
There is also asserts condition (without is) for asserting a plain boolean, which narrows based on
the condition — useful for assert(x !== null) style checks.
When to write one — and when not to
Type guards and assertion functions are powerful, but do not reach for them when a built-in narrowing works:
- A union of primitives? Use
typeof— no guard needed. - A discriminated union? Check the discriminant — no guard needed.
- Objects differing by a property? Use
in— no guard needed. - Class instances? Use
instanceof— no guard needed.
Write a custom type guard when the check is genuinely custom — validating the shape of external data, a condition that spans multiple properties, or a runtime check TypeScript cannot express otherwise. And for validating external data specifically, a schema library like Zod (real-projects module) generates the guard for you and does the runtime check reliably — often better than a hand-written guard. Reach for a hand-written guard when the check is small and specific; reach for a validation library when you are parsing a whole API response.
Check your work
Why a boolean-returning check does not narrow. It tells TypeScript nothing about the argument's
type; inside the if, the value is unchanged.
The type-predicate return type. value is Type — "when this returns true, the argument is Type" —
which makes the function a narrowing type guard.
The responsibility a type guard carries. TypeScript trusts the predicate; the safety is only as good as your runtime check.
Guarding array elements. filter((v): v is string => ...) narrows the result to string[].
What an assertion function does. Throws if the condition fails; its asserts value is Type return
narrows the value for all code after the call, no if needed.
asserts value is Type versus asserts condition. The first narrows a value to a type; the second
narrows based on a boolean condition.
When to write a custom guard. For genuinely custom checks (external-data shape, multi-property
conditions) — not when typeof/in/instanceof/a discriminant works.
The better tool for whole API responses. A validation library like Zod, which does the runtime check and produces the type.
Practice
- Write
isUserreturning a plainbooleanand confirm the value staysunknowninside theif. - Change the return type to
value is Userand confirm the value now narrows. Access a property inside the branch. - Deliberately write a guard that only checks
namebut claimsvalue is User. Note TypeScript trusts it — and reason about the bug this could cause. - Use
filter((v): v is string => v !== null)on a(string | null)[]and confirm the result isstring[]. - Write an
assertIsUserassertion function and confirm the value narrows after the call, with noif. - Use
asserts conditionto writeassert(x !== null)and confirm it narrows. - For four checks, decide whether a built-in narrowing (
typeof/in/instanceof/discriminant) or a custom guard is right, and justify each.
Official documentation
- TypeScript — Using type predicates — Custom type guards.
- TypeScript — Assertion functions —
asserts value is Type. - Zod — Runtime validation that generates guards for external data.
Next: exhaustiveness checking with never.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship