Assertion functions
Throw at the boundary so the rest of the code never has to ask whether the value is right.
A predicate gives you an if. An assertion gives you a sentence. After the assertion runs without throwing, the value is whatever you said it is — for the rest of the function. No nested branches, no early returns to remember. This lesson is about that flavor of check, when to reach for it, and the small bit of TypeScript syntax that makes it work.
The asserts keyword
A regular function can throw. TypeScript doesn't normally read anything into that — after the call returns, the type of your value is the same as before. The asserts keyword changes that.
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new TypeError("expected a string, got " + typeof value);
}
}
function loudUpper(value: unknown) {
assertIsString(value);
return value.toUpperCase(); /* value is string here */
}The signature reads "asserts value is string." That phrase tells the compiler two things: the function might throw, and if it returns normally, the argument is a string from that line on. You don't wrap the call in an if. You don't store the result. The narrowing flows out of the call site.
The body is your job. The compiler trusts the signature; it never checks whether the body actually verifies the claim. Same trust contract as a predicate.
An assertion function with a return value is fine, but the body can never use return to short-circuit — it must throw on failure. A function that quietly returns when the value is wrong defeats the contract.
function assertIsNumber(v: unknown): asserts v is number and the body returns undefined when v is a string instead of throwing. What happens when a string is passed?Assertion vs predicate
Both check shape. Both teach the compiler. They differ in flow.
A predicate gives you a branch. You decide what to do with false — throw, return a fallback, log and skip. The flexibility costs you a level of indentation everywhere.
An assertion is a fence. The function either succeeds and lets you through, or it throws. There is no "what to do on failure" question — the call site doesn't get to decide.
function loadUser(raw: unknown) {
if (!isUser(raw)) {
throw new Error("bad user");
}
return raw.name; /* User */
}function loadUser(raw: unknown) {
assertIsUser(raw);
return raw.name; /* User */
}Same outcome, less code in the assertion version. The win is bigger when you have several values to check at the top of a function — three predicate if blocks become three flat assertion calls, and the rest of the function reads as if all three values were already typed.
Use a predicate when failure is a real path you want to handle gracefully (a fallback render, a "skip this row," a "ask again"). Use an assertion when failure is a bug the caller can't recover from — typically right at the trust boundary, where bad data means the operation simply cannot continue.
Asserting truthiness
There's a shorter form: asserts value — no is X after it. This narrows away the falsy possibilities (null, undefined, 0, "", false).
function assertDefined<T>(value: T): asserts value {
if (!value) {
throw new Error("expected a defined value");
}
}
function findUser(id: string) {
const user = cache.get(id); /* User | undefined */
assertDefined(user);
return user.name; /* User */
}The signature asserts value is the truthiness flavor: after the call, value cannot be falsy. This is handy for the common case where a lookup returns T | undefined and you've already convinced yourself the value will be there. It's also dangerous for the same reason — if the assumption is wrong, your assertion is a louder bug than the original undefined would have been.
A small upgrade: take a label and bake it into the error message so the throw is debuggable.
function assertDefined<T>(
value: T,
label: string,
): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error(label + " was null or undefined");
}
}
assertDefined(user, "user from cache");The variant uses asserts value is NonNullable<T> — a slightly stronger contract. NonNullable<T> is TypeScript's built-in utility type that strips null and undefined from a type. After the call, the value is T minus the nullish parts. The label makes the eventual Error.message actually useful in a stack trace.
Don't use assertDefined as a silent bypass for values that legitimately could be missing. It's for cases where missing means "bug in our code." Real "could be missing" should be handled with a check, not asserted away.
x is non-null after the call?Patterns at the boundary
A common shape: read raw data, assert its shape, return the typed value. The assertion lives at the gate, and every caller after it gets a real type.
type User = { name: string; age: number };
function assertIsUser(value: unknown): asserts value is User {
if (
typeof value !== "object" ||
value === null ||
typeof (value as User).name !== "string" ||
typeof (value as User).age !== "number"
) {
throw new Error("not a User: " + JSON.stringify(value));
}
}
async function loadUser(id: string): Promise<User> {
const res = await fetch("/api/users/" + id);
const data: unknown = await res.json();
assertIsUser(data);
return data; /* User */
}loadUser returns Promise<User> honestly. There is no as User lie anywhere. If the server response shape changes, the assertion throws a useful message — not a TypeError: cannot read property 'name' of undefined 30 lines later.
The same pattern works for arguments at a public API surface — a library function that takes unknown and asserts the shape before doing anything else. From the inside, the function reads as if the value were always typed.
export function calculateTax(input: unknown): number {
assertIsOrder(input);
return input.items.reduce((sum, item) => sum + item.price * 0.08, 0);
}The user of the library passes whatever they have. Inside the function, after the assertion, input.items is fully typed. The boundary lives in one place; the rest of the function never has to wonder.
assertIsX(data) at the fetch site. Which problem does this solve?assertIsUser and need to use it on an array of unknowns. Which approach scales best?