10.5 Assertion Functions: asserts x is T
Alright, let’s talk about asserts x is T. This is one of those TypeScript features that feels like a superpower once you get it, but first, it looks like the language designers had a bit too much coffee. It’s not magic; it’s just a very clever, formal way of telling the compiler, “Hey, trust me on this one, and by the time this function is done, I promise the type of this variable will be different.”
You’ve already met type guards, those helpful little functions that return a boolean and let you narrow a type within a conditional block. asserts x is T is its more assertive, slightly more dramatic cousin. Instead of returning a boolean and letting you handle the control flow, an assertion function throws an exception if the condition isn’t met. It doesn’t ask “is this a fish?”; it grabs the animal, checks for gills, and if it doesn’t find them, it chucks the poor thing back into the ocean with an error. It’s a function that makes a guarantee: if it returns at all, the assertion has passed.
The Basic Anatomy
Here’s the simplest, most classic example: defending against null or undefined.
function assertIsDefined<T>(value: T, message?: string): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new Error(message || `Expected value to be defined, but got ${value}`);
}
}
Let’s break down the signature. asserts value is NonNullable<T> is the key. It’s a type predicate on steroids. It tells the compiler: “After you call me, and assuming I don’t throw, you can assume for the rest of this scope that value is no longer T | null | undefined; it’s just T.”
Now, watch it work:
let maybeString: string | undefined = fetchFromSomewhereDangerous();
// At this point, TypeScript knows maybeString is string | undefined.
// You can't safely do string operations.
console.log(maybeString.length); // Error: Object is possibly 'undefined'
// We call our assertion function.
assertIsDefined(maybeString, "Dangerous fetch failed!");
// Now, the compiler knows for a FACT that maybeString is a string.
// The function didn't return void; it returned 'asserts value is string'.
// If it didn't throw, the assertion held true.
console.log(maymeString.length); // Works perfectly.
This is incredibly useful for validating inputs, configuration, or API responses where you want to fail fast and loudly if your assumptions are wrong.
Why Not Just Use a Type Guard?
It’s a fair question. Sometimes you want the boolean! A type guard is perfect for when you have two valid code paths.
// Using a type guard - you handle both outcomes
if (isString(myValue)) {
// do string things
} else {
// do other things
}
An assertion function is for when there is no valid other outcome. It represents an invariant, a hard requirement for your program to continue. If the value isn’t a string, the current function execution is invalid and cannot proceed. It’s a paradigm shift from “check” to “ensure.”
The Devil’s in the Details: Pitfalls and Edge Cases
This power comes with responsibility. Here are the sharp edges to watch for.
1. Control Flow is Everything. The narrowing only applies to the code after the function call. This is obvious but crucial. The compiler’s control flow analysis is smart enough to understand that if the function returns, the assertion must be true. If it throws, well, the rest of your code doesn’t run anyway.
2. It’s a One-Way Street. You can narrow a type, but you can’t arbitrarily change it to something unrelated. asserts x is string works if x started as string | number. But asserts x is boolean won’t work from that same union; the compiler rightly won’t let you make a claim it can’t possibly verify. The asserted type must be a subset or a more specific version of the original type.
3. Void is the Only Allowed Return Type (Sort Of). The official return type of an assertion function must be asserts x is T. Under the hood, this is compatible with void. You cannot return a meaningful value from an assertion function. Its job is to assert and then get out of the way. If you try to return a value, you’ll get a type error. This is one of those questionable choices—it can feel limiting. Sometimes you want to assert and return the value. You can work around it, but it’s a bit clunky:
// You can't do this:
function assertAndReturn<T>(value: T | null): asserts value is T {
if (value === null) throw new Error();
return value; // Type error! An assertion function must return 'void'.
}
// You have to split it into two functions:
function assertIsNotNull<T>(value: T | null): asserts value is T {
if (value === null) throw new Error();
}
function getValue(): string | null { ... }
const val = getValue();
assertIsNotNull(val); // narrows val to string
// Now use val
A More Complex, Real-World Example
Let’s move beyond null checking. Imagine you’re parsing a JSON API response. You have a rough shape, but you need to validate it’s what you expect.
interface User {
id: number;
username: string;
email: string;
}
// A type predicate for comparison
function isUserResponse(data: any): data is User {
return (
data &&
typeof data.id === 'number' &&
typeof data.username === 'string' &&
typeof data.email === 'string'
);
}
// The assertion function version
function assertIsUserResponse(data: any): asserts data is User {
if (!(data && typeof data.id === 'number' && typeof data.username === 'string' && typeof data.email === 'string')) {
throw new Error("Received data is not a valid User response");
}
}
// Usage:
const rawData: any = await fetchUser();
assertIsUserResponse(rawData); // Throws a descriptive error if invalid
// Now, TypeScript knows rawData is a User. No more 'any'!
console.log(rawData.email.toLowerCase()); // Perfectly safe
The assertion function is the better choice here. There’s no sensible fallback if the API returns malformed data; you want to fail immediately and explicitly, and this pattern enforces that beautifully.
In essence, asserts x is T is your tool for creating guaranteed contracts within your code. It makes your assumptions explicit and your runtime checks work directly with the type system. It turns what would be a runtime error into a compile-time-known type, which is just brilliant. Use it to fail fast, fail clearly, and write incredibly robust code.