skip to content

User-Defined Guards

When the built-in checks are not enough you teach the compiler your own narrowing with type predicates and assertion signatures. Interviewers care because these are exactly the places where you, not the checker, are vouching for correctness.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

12

In TypeScript, the function `function isString(x: unknown): x is string { return typeof x === 'number'; }` compiles with no error at all. What does the compiler actually verify about a type predicate, and what does it leave entirely to you?

level: juniorimportance: must knowfreq 70%

answer

  1. the compiler takes your word
  2. only one rule is enforced
  3. assignable to the parameter type
  4. the body is never inspected
  5. return true narrows everything

basics

~20 s

TypeScript checks only that the asserted type is assignable to the parameter's declared type. It never inspects the body, so a guard that returns true always narrows, and a wrong predicate is an unchecked assertion the compiler trusts.

solid answer

~40 s

A `x is T` return type is a promise the checker takes on faith. The single rule it enforces is that `T` must be assignable to the parameter's declared type — `function f(x: string): x is number` is rejected with "A type predicate's type must be assignable to its parameter's type". Beyond that, the body is ordinary code: as long as it returns a boolean, `return true`, a typo'd `typeof` comparison, or a check of the wrong property all compile. Because the parameter is usually `unknown`, that assignability rule buys nothing, so the predicate is effectively an `as T` that runs at the call site. The compiler then narrows every downstream use on the strength of a claim nobody verified, and the failure shows up much later as `undefined` where a value was guaranteed.

go deeper

for a junior

Be able to say plainly that TypeScript trusts the predicate: it only checks that the asserted type fits the parameter type, never the body. Show that return true compiles.

for a middle

Explain the assignability rule with an example it rejects, and why it is vacuous for an unknown parameter. Connect it to erasure — there is no runtime type information for the compiler to check against.

for a senior

Demonstrate that you treat guards as a boundary you own: the failure lands far from the guard, so the check must cover every declared field and be tested like production code. Mention inferred predicates where they apply.

for a principal

Own the policy question: where guards are permitted at all versus where a schema validator must produce type and check from one source, and how you keep hand-written guards from silently drifting as types evolve.

## What a type predicate is A user-defined type guard is an ordinary function whose return type is written `parameterName is SomeType` instead of `boolean`: ```ts function isString(x: unknown): x is string { return typeof x === 'number'; // wrong, and it compiles } ``` When the checker sees `if (isString(v))`, it narrows `v` to `string` in the true branch and removes `string` from `v`'s type in the false branch. That narrowing is the entire payoff, and it happens purely at compile time. ## The one thing the compiler checks The asserted type must be assignable to the parameter's *declared* type. This is the only rule: ```ts declare function bad(x: string): x is number; // error: predicate type not assignable declare function ok1(x: unknown): x is number; // fine — everything fits unknown declare function ok2(x: string | number): x is number; // fine declare function ok3(x: object): x is Date; // fine — Date is an object ``` The error message is "A type predicate's type must be assignable to its parameter's type." Notice what this rule cannot do for you: guards are almost always written against `unknown`, and *every* type is assignable to `unknown`, so in the common case the rule is vacuous. You can claim anything about an `unknown` parameter. ## What it does not check The body. Not one clause of it. The compiler treats the function as returning `boolean` and then, at each call site, reinterprets that boolean as evidence about the argument. All of these compile: ```ts function isUser(x: unknown): x is User { return true; } function isAdmin(x: unknown): x is Admin { return typeof x === 'object'; } function isOrder(x: unknown): x is Order { return 'id' in (x as object); } ``` None of them is an error, and none of them establishes what it claims. ## Why it works this way TypeScript's types are erased: the emitted JavaScript contains no types, no shape descriptors and no reflection, so there is nothing at runtime the compiler could compare a value against. It cannot generate the check for you, and it cannot prove your hand-written check is equivalent to the type, because that would mean symbolically executing arbitrary JavaScript. So the design is explicit: the predicate is the point where *you* take responsibility, the same way `as` is. The difference is ergonomic, not semantic — a predicate is a cast with better manners. It reads like validation, it lives in one place, and it can genuinely be correct, but the type system gives you no more assurance than a cast does. ## The blast radius A cast is local and visible; a bad guard is neither. Once `isOrder` returns true, every downstream function receives a fully-typed `Order`, and the checker will happily let you read `order.items.length`. The wrongness surfaces far from the guard, as a `TypeError: Cannot read properties of undefined`, in code that looks perfectly type-safe. That distance is exactly why interviewers ask: the guard is where a whole codebase's type safety is either earned or forged. ## The one case the compiler does derive the predicate Since TypeScript 5.5, a function with **no** explicit return type annotation whose body is a single boolean expression over an un-reassigned parameter can have a type predicate *inferred*: ```ts function isDefined(x: string | undefined) { return x !== undefined; // inferred as: x is string } const names: string[] = ['a', undefined, 'b'].filter(isDefined); ``` This is the sound direction: the predicate is derived from the check rather than asserted over it, so it cannot disagree with the body. It only applies to simple cases, but when it fires it is strictly better than writing the annotation by hand. ## What to do with this knowledge Write the check first and the signature second, and make the body cover every field the asserted type declares. Prefer guards over `as` because they at least concentrate the risk in one reviewable function — then review that function like the security boundary it is. For values crossing a real boundary (parsed JSON, an API response, `localStorage`), let a schema validator produce both the runtime check and the static type from one definition, so there is nothing left to keep in sync by hand.

  • You said the compiler enforces one rule — show me a predicate it actually rejects.
    `function isNum(x: string): x is number` is rejected: the asserted type must be assignable to the parameter's declared type, and `number` is not assignable to `string`. It is a sanity check against nonsense, not validation. Because real guards take `unknown`, and everything is assignable to `unknown`, the rule almost never fires where it would matter.
  • Is there any situation where TypeScript derives a type predicate instead of trusting one?
    Yes. Since TypeScript 5.5, a function with no explicit return type whose body is a single boolean expression over a parameter it never reassigns gets a type predicate inferred — `function isDefined(x: string | undefined) { return x !== undefined; }` is inferred as `x is string`. Because the predicate comes from the check, the two cannot disagree.
  • When a guard lies, where does the program actually break?
    Nowhere near the guard. The call itself just returns a boolean; narrowing is compile-time only, so nothing throws at the `if`. The bad value flows into code the checker believes is safe, and the failure appears later as a `TypeError` on a property access, or as `undefined` written into a database — often in a module that never touched the guard.
  • If a predicate is no safer than a cast, why prefer it?
    Because it concentrates the risk. A cast is repeated at every boundary and is invisible in review; a guard puts the claim in one named function you can test, and it composes with control-flow analysis so the false branch is narrowed too. The type system's guarantee is identical — the engineering difference is that one place can be made right.

saying these in an interview costs you the question

  • Thinks the compiler verifies the guard body matches the type
  • Believes a type predicate performs the runtime check for you
  • Says returning true from a guard is a compile error
  • Assumes types exist at runtime so tsc can compare shapes
  • Claims a predicate is inherently safer than an as cast

context

open as a page

In TypeScript, a utility module exports `function isString(value: unknown): value is string`. What does the `value is string` return type mean, and what type does `value` have inside `if (isString(value)) { ... }`?

level: juniorimportance: must knowfreq 62%

basics

~20 s

The return type value is string makes isString a user-defined type guard: it returns an ordinary boolean at runtime, but the compiler treats a true result as proof and narrows value to string inside the if-branch.

open as a page

In TypeScript, how does a user-defined guard declared `asserts value is User` differ from one declared `value is User`, and when would you reach for each?

level: middleimportance: must knowfreq 55%

basics

~20 s

A value is User guard returns a boolean and narrows only where that boolean is tested; an asserts value is User guard returns nothing, throws on failure, and narrows the value for all code after the call.

open as a page

A service validates API responses with `function isOrder(x: unknown): x is Order { return typeof x === 'object' && x !== null && 'id' in x; }`, where `Order` is `{ id: string; items: Item[]; total: number }`. Which bad payloads still pass this guard, and how deep does a trustworthy check have to go?

level: middleimportance: must knowfreq 58%

basics

~20 s

The guard only proves the value is a non-null object with some property named id. Arrays pass, id may be a number, and items and total are never examined — so a trustworthy check must test every declared field, recursively, and array elements one by one.

open as a page

In TypeScript, a helper is written as `function isString(value: string | number): boolean { return typeof value === 'string'; }`. Callers then do `if (isString(x)) { x.toUpperCase(); }` and the compiler rejects `x.toUpperCase()`. Why does the boolean return type not narrow, and what change fixes it?

level: middleimportance: must knowfreq 70%

basics

~20 s

A boolean return type says nothing about which argument was checked, and the checker never looks inside a called function. Declaring the return type as value is string turns the helper into a type predicate, so callers narrow.

open as a page

In TypeScript, a helper is declared `function assertIsString(value: unknown): asserts value is string`. What does that return type tell the compiler, and what must the body do when value is not a string?

level: juniorimportance: should knowfreq 35%

basics

~10 s

It declares an assertion function: returning normally is the compiler's proof, so from the call onward TypeScript treats value as a string. The body must throw when value is not a string.

open as a page

In TypeScript, calling this helper reports "Assertions require every name in the call target to be declared with an explicit type annotation": `const assertIsString = (v: unknown): asserts v is string => { if (typeof v !== "string") throw new TypeError(); };`. Why is the call rejected, and how do you fix it?

level: middleimportance: should knowfreq 42%

basics

~20 s

An assertion call only works when every name in the call target has a written-out type, and this const's type is inferred from its arrow initializer. Fix it by annotating the binding with a function type, or by using a function declaration.

open as a page

In TypeScript, given `const names: (string | undefined)[]`, what type does `names.filter(n => n !== undefined)` produce, and how do you guarantee the result is `string[]` regardless of the compiler version?

level: middleimportance: should knowfreq 55%

basics

~20 s

On TypeScript 5.5 and later the compiler infers a type predicate for that arrow and the result is string[]; on earlier versions it stays (string | undefined)[]. Passing a function declared with a value is string return type always gives string[].

open as a page

You want one TypeScript helper, invariant(condition, message), that throws when the condition is falsy and also narrows types for the code that follows. What signature makes that work, and what does a call like `invariant(typeof id === "string")` narrow?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Declare it with the bare assertion form: function invariant(condition: unknown, message?: string): asserts condition. After the call the compiler applies whatever narrowing the condition expression would have produced inside an if, so the id binding becomes string.

open as a page

A codebase fetches JSON with `async function getJson<T>(url: string): Promise<T> { const res = await fetch(url); return res.json(); }`. Why is this signature a type-safety hole, and what would you return instead?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Nothing decides T except the caller's annotation, and the body compiles only because the DOM lib types Response.json() as Promise<any>. The signature is a disguised cast. Return Promise<unknown> and force a guard, or take a validating parser that infers T.

open as a page

A team hand-writes `interface Order` and a separate `isOrder(x: unknown): x is Order` guard. Months later a required field is added to `Order`, the guard is never updated, and nothing fails to compile. Why does nothing break, and how would you stop this class of drift?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Predicate bodies are never checked against the asserted type, so a guard silently keeps promising a shape it no longer verifies. The durable fix is one source of truth: derive the static type from a runtime schema so the check cannot fall behind the type.

open as a page

In TypeScript, what does a method whose declared return type is `this is Foo` do at call sites, and when would you model a check that way instead of exporting a standalone `value is Foo` guard?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

A method returning this is Foo is a type predicate on the receiver: when it returns true, the object you called it on is narrowed to Foo for the rest of the branch. Reach for it when the object owns the state being checked.

open as a page