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?
answer
- the compiler takes your word
- only one rule is enforced
- assignable to the parameter type
- the body is never inspected
- return true narrows everything
basics
~20 sTypeScript 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 sA `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
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.
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.
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.
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