Built-in checks narrow unknown for typeof, in and instanceof, but custom validation needs a way to tell the compiler what a function proved. A type predicate (value is T) narrows the true branch; an assertion function (asserts value is T) narrows the rest of the scope by throwing when the check fails.
Before you start
You should be comfortable with unknown, union types and control-flow narrowing, and you should have strict mode on. This article covers predicates and assertion signatures; it assumes you already separate “what the compiler proves” from “what the runtime must verify”.
Step-by-step walkthrough
Step 1: Write a user-defined type guard
Annotate the return type as value is T and return a real boolean check. Inside an if, the compiler narrows value to T in the true branch and to the remaining type in the else branch. The predicate is a claim about the runtime: a predicate that returns true without checking is a lie the compiler cannot detect.
Step 2: Use an assertion function when failure throws
An assertion function has the return annotation asserts value is T and throws when the condition is not met. After the call, the compiler treats value as T for the rest of the block. The consequence is that the function must either throw or the value must already satisfy T; there is no boolean to ignore.
Step 3: Keep the checks honest and specific
Validate the exact shape the predicate claims: check every required field and the discriminant, not just typeof value === 'object'. Prefer small predicates that compose (for example hasId then hasName) over one predicate that claims a large interface from a shallow test.
Worked scenario
The guard narrows an element of an unknown[]; the assertion makes a required value usable without a cast.
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function firstUppercase(values: unknown[]): string | undefined {
if (values.length > 0 && isString(values[0])) {
return values[0].toUpperCase(); // narrowed to string
}
return undefined;
}
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Expected a defined value');
}
}Walk through the example
isString returns a boolean the compiler understands, so values[0] becomes string only inside the guarded branch. assertDefined narrows by control flow: after the call, code that follows sees T instead of T | null | undefined. Without the annotation, both functions would still run, but the compiler would keep the wide type and force a cast.
Common mistake
Writing a predicate that is broader than the check. function isUser(v: unknown): v is User { return typeof v === 'object'; } claims a full User from a shallow test, so later code trusts fields that may be missing. Assertion functions are also easily abused: they must be declared with an explicit type and are not written as arrow functions, precisely because they change the flow of the code after the call.
Verify the behavior
Call the guard with each boundary value — null, an array, a number, and a valid string — and check that only the valid case narrows. For the assertion function, confirm it throws on null and undefined and that the code after it compiles without a cast. Type-check the file with npx tsc --noEmit after deliberately returning true from a predicate to see that the compiler accepts the lie, which is the point.
Interview exercise
You parse JSON to unknown. Do you write an assertion function or a predicate, and why?
Answer and reasoning
Prefer a predicate such as isOrder(value): value is Order that returns a boolean, because invalid input is a normal, recoverable outcome at a boundary. Use an assertion function only where a failure truly is exceptional and ending the current flow is correct, or inside a larger parseOrder that converts a boolean result into an explicit error. The choice encodes whether invalid data is expected or fatal.
Follow-up discussion
Can a predicate narrow to a union member? Yes — value is 'a' | 'b' or value is Admin | Editor is common when a discriminant is present. Do assertion functions work with optional chaining? No: an assertion call must be a standalone statement, so it cannot be the right-hand side of ?. or buried in a larger expression.
Continue learning
See the boundary pattern in TypeScript unknown at API boundaries and combine guards with discriminated unions. Read the TypeScript narrowing handbook and try the TypeScript interview questions.