Lesson 43 of 80
x is T and asserts x is T both narrow a type — but one narrows inside a branch, the other narrows everything after it, for the rest of the scope.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The custom type guard lesson covered x is T, a type predicate: a function that returns boolean, and whose return type tells TypeScript "if this comes back true, narrow the argument to T" inside whichever if branch checked it.
An assertion function does something related but structurally different. Instead of returning a boolean you check in an if, it returns void — and either throws or lets execution continue. Its signature uses asserts x is T instead of x is T. The payoff isn't a narrowed branch; it's a narrowed rest of the scope, starting right after the call, with no if at all.
function assertIsUser(x: unknown): asserts x is User {
if (typeof x !== "object" || x === null || !("id" in x)) {
throw new Error("Not a User");
}
}
function greet(data: unknown) {
assertIsUser(data);
console.log(data.id); // ✅ narrowed to User for everything below this line
}
There's no branch to be inside of. TypeScript trusts that if assertIsUser returns normally (doesn't throw), data really is a User — so it narrows unconditionally from that point forward, the same way it would after a successful if (isUser(data)) check, just without the indentation.
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null && "id" in x;
}
function greetA(data: unknown) {
if (isUser(data)) {
console.log(data.id); // narrowed only inside this block
}
// data is still `unknown` here
}
function greetB(data: unknown) {
assertIsUser(data);
console.log(data.id); // narrowed for the rest of greetB, unconditionally
}
Same underlying check, two different control-flow shapes. isUser is for when invalid input is a normal case you want to branch on — render a fallback, skip an item, try something else. assertIsUser is for when invalid input means the function genuinely cannot continue — a guard clause at the top of a function, the same instinct as an assertion library, but one the compiler actually understands and narrows from.
asserts x is T is still a promise, not a proof — exactly like a type predicate. TypeScript doesn't verify that your logic actually matches T's shape; it trusts the signature and narrows accordingly, so a buggy assertion function is just as capable of lying to the compiler as a buggy type guard.return never, or loop forever. If it just logs a warning and returns, TypeScript still narrows afterward, because all it's watching is "did this call return normally," not what the function's body actually does when the check fails.if condition. if (assertIsUser(data)) doesn't compile — an assertion function's return type is void, not boolean, so there's nothing to branch on. It's a statement you call and move past, not a check you evaluate.const need an explicit asserts return annotation to work reliably. Unlike a type-predicate arrow function, which TypeScript can often infer cleanly, leaving the annotation off an assertion arrow function is a common way to end up with a plain void-returning function that narrows nothing."When would you write an assertion function instead of a type guard?" tests whether someone sees these as two answers to the same question — narrowing — rather than interchangeable syntax. The real answer is about control flow: a type guard for a branch you want to take conditionally, an assertion function for a precondition that should stop the function outright if it's not met.