Assertion Functions
A type guard returns a boolean and lets you branch; an assertion function instead throws when a value is not what you expect and narrows the type for everything after the call. Its return type uses the asserts keyword — either asserts x is T to narrow to a specific type, or asserts x to assert that a value is truthy. This mirrors Node’s assert module but teaches the compiler what a successful call implies. This page covers both forms, narrowing unknown, the no-implicit-return rule, and how assertion functions compare to type guards.
The asserts x is T form
An assertion function’s return annotation is asserts param is Type. If the function returns normally, the compiler narrows param to Type from that point on; if the value is wrong, the function must throw.
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new TypeError(`Expected string, got ${typeof value}`);
}
}
function toUpper(input: unknown): string {
assertIsString(input);
// input: string (narrowed for the rest of the scope)
return input.toUpperCase();
}
Unlike a type guard, there is no if branch — after assertIsString(input) the narrowing applies to the remainder of the function, because reaching that line proves the value passed.
The asserts x (truthiness) form
Omitting is Type gives asserts value, which asserts the argument is truthy and narrows away null, undefined, and other falsy types.
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function getLength(text: string | null): number {
assert(text, "text must not be null");
// text: string (null removed)
return text.length;
}
This is the classic assert(condition) helper, now type-aware: after the call, TypeScript knows condition was truthy.
Asserting object shapes from unknown
Assertion functions shine at trust boundaries, validating parsed data and narrowing it to a domain type in one statement.
interface User {
id: number;
name: string;
}
function assertIsUser(value: unknown): asserts value is User {
if (
typeof value !== "object" ||
value === null ||
typeof (value as Record<string, unknown>).id !== "number" ||
typeof (value as Record<string, unknown>).name !== "string"
) {
throw new Error("Invalid User payload");
}
}
const data: unknown = JSON.parse('{"id":1,"name":"Ada"}');
assertIsUser(data);
console.log(data.name); // data: User
The single assertIsUser(data) call replaces a nested if and leaves data typed as User afterward.
The implicit-return rule
For the narrowing to be sound, an assertion function must either throw or return normally — it cannot have an inferred return that silently allows continuing on a bad value. The asserts annotation is mandatory; you cannot apply it to an arrow function with an inferred type expression.
// ❌ Error: Assertions require every name in the call target to be
// declared with an explicit type annotation.
const assertNum = (x: unknown): asserts x is number => {
if (typeof x !== "number") throw new Error("not a number");
};
Assign assertion functions as named
functiondeclarations (or explicitly typed members). TypeScript requires the call target’s type to be statically known, so inline untyped arrows are rejected.
Assertion functions vs type guards
Both narrow, but they suit different control flow. Choose based on whether an invalid value is recoverable.
| Aspect | Type guard (x is T) | Assertion (asserts x is T) |
|---|---|---|
| Returns | boolean | void (throws on failure) |
| Control flow | branch with if | linear; throws otherwise |
| Narrows | inside the true branch | rest of the scope |
| Use when | invalid input is expected/handled | invalid input is a bug |
Use a guard when both outcomes are normal; use an assertion when an invalid value means a programming error you want to fail fast on.
Best Practices
- Annotate with
asserts x is Tto narrow to a type, orasserts xto assert truthiness. - Make the function genuinely throw on failure — like guards, the compiler trusts your logic.
- Declare assertion functions as named
functions (or explicitly typed members); inline arrows are rejected. - Use them at trust boundaries to validate
unknowndata and narrow in a single statement. - Prefer assertions when invalid input is a bug to fail fast; prefer guards when both branches are expected.
- Keep them small and pure, and name them
assertXfor clarity.