TypeScript 84 🔷 Assertion Functions and asserts
An assertion function is a function that validates a condition at runtime and, if the condition fails, throws an error. If the function returns normally, TypeScript knows the condition was true and narrows the type of the relevant variable for the remainder of the scope . The asserts keyword in the return type is what tells the compiler this: asserts value is string means “if this function returns, value is a string” .
This is different from a type predicate (value is string), which returns a boolean and must be used in a conditional . It is also different from a type assertion (value as string), which performs no runtime check at all and will happily let you call string methods on a number, producing NaN or a crash with no compiler complaint . Assertion functions sit between the two: they validate at runtime like a type predicate, but they narrow the type unconditionally in the calling scope like an assertion .
Key point: The asserts keyword tells TypeScript that the function’s successful return implies the condition is true. The function must throw if the condition is false. There is no boolean return value — the narrowing happens because control flow reaching the next line means the function did not throw .
Why assertion functions exist
A type predicate used in an if check narrows the type only inside the if block. An assertion function narrows the type for the rest of the scope, without a conditional wrapper.
The repeated check problem. Every function that needs a non-null Order repeats the same if (order === null) throw ... pattern . The check is three lines, it is duplicated in processOrder, shipOrder, and any other function that touches an Order, and the only difference between copies is the error message. An assertion function centralizes the check: assertDefined(order, "order") replaces the three lines and narrows order to Order for the rest of the function .
The unknown data problem. Data from response.json(), JSON.parse, or a form input is typed unknown or any. A type assertion (as User) silences the compiler but does nothing at runtime — if the API returns an error object, data.email is undefined and the bug surfaces later . An assertion function validates the shape and throws immediately if it does not match, giving the caller a typed value and a clear error .
The type guard limitation. A type guard returns a boolean, so the caller must wrap every use in a conditional. This is fine when the check is localized — you are testing a value inside one if. It is inconvenient when the check is a precondition for the entire function. Assertion functions encode preconditions directly in the control flow: call the function, and the rest of the scope assumes the validated type .
The trade-off. Assertion functions throw. That means the caller must be prepared for an exception, either via try/catch or by accepting that the program will crash on invalid input. This is the right behavior for preconditions that should never be violated (a function that requires a valid order should not silently return undefined). It is the wrong behavior for validation that should produce a user-friendly error message, where returning a Result type or a boolean is better .
a. The asserts Return Type
An assertion function’s return type begins with asserts. There are two forms: asserts condition and asserts value is Type .
The asserts condition form asserts that a boolean expression is true. The function accepts the condition as a parameter and throws if it is falsy :
function assert(condition: unknown, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg ?? "Assertion failed");
}
}
function point(x: unknown, y: unknown): { x: number; y: number } {
assert(typeof x === "number", "x must be a number");
assert(typeof y === "number", "y must be a number");
return { x, y }; // x and y are narrowed to number
}
The asserts value is Type form asserts that a specific value has a specific type. This is the form used for validation functions :
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Value must be a string");
}
}
function printUpperCase(value: unknown) {
assertIsString(value);
console.log(value.toUpperCase()); // value is narrowed to string
}
The function body must throw when the assertion fails. TypeScript does not verify that you actually throw — you could write an empty body and the compiler would accept it, but the type narrowing would be a lie . The compiler trusts the assertion signature. This is why assertion functions are dangerous when written carelessly: they are a contract with the compiler, and the compiler takes your word for it .
The function returns void in both forms. It does not return the narrowed value. The narrowing applies to the variable passed as an argument .
b. Assertion Functions vs Type Predicates
Type predicates and assertion functions both narrow types at runtime, but they are used differently.
A type predicate returns a boolean and is used in a conditional :
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(value: unknown) {
if (isString(value)) {
// value is string inside this block
console.log(value.toUpperCase());
} else {
// value is still unknown here
}
}
An assertion function returns void (implicitly) and throws on failure. It is called as a statement, and the narrowing applies to the rest of the scope :
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Not a string");
}
}
function process(value: unknown) {
assertIsString(value);
// value is string for the rest of the function
console.log(value.toUpperCase());
}
The choice between them is about control flow. If the invalid case has a meaningful branch — you want to handle a non-string differently but not crash — use a type predicate and an if/else. If the invalid case is a bug or an unrecoverable precondition — you want the program to stop or the caller to catch — use an assertion function .
Type predicates are also composable in expressions: const valid = isString(x) && isNumber(y). Assertion functions are statements, not expressions, because they do not return a value that can be combined .
c. Assertion Functions vs Type Assertions
A type assertion (as) tells the compiler “trust me, this is the type.” It performs no runtime check. If you are wrong, the error appears later, far from the assertion, as a runtime TypeError or a silently incorrect value .
An assertion function checks at runtime and throws immediately if the check fails. The error appears at the point of validation, with a message you control .
// Type assertion — no runtime check
const data = await response.json();
const user = data as User; // compiler trusts you
console.log(user.email); // crashes if data has no email
// Assertion function — runtime check
const data = await response.json();
assertIsUser(data); // throws here if invalid
console.log(data.email); // safe, data is User
The assertion function is strictly safer for external data. The type assertion is appropriate only when the type information was lost but you have independent evidence the value is correct — for example, after a manual check that the compiler cannot see .
Complete Example Session
This session builds an assertion helper, a validation function for API responses, and demonstrates the generic non-null assertion.
// ============================================
// PART 1: THE BASIC ASSERT
// ============================================
function assert(condition: unknown, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg ?? "Assertion failed");
}
}
function divide(a: number, b: number): number {
assert(b !== 0, "Division by zero");
return a / b; // b is narrowed to number
}
// ============================================
// PART 2: THE TYPE-SPECIFIC ASSERT
// ============================================
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function shout(value: unknown): string {
assertIsString(value);
return value.toUpperCase() + "!";
}
// ============================================
// PART 3: THE GENERIC NON-NULL ASSERT
// ============================================
function assertDefined<T>(
value: T | null | undefined,
name: string
): asserts value is T {
if (value === null || value === undefined) {
throw new Error(`${name} must be defined`);
}
}
function processUser(user: { name: string } | null): void {
assertDefined(user, "user");
console.log(user.name); // user is { name: string }
}
// ============================================
// PART 4: THE COMPLEX OBJECT ASSERT
// ============================================
interface Address {
street: string;
city: string;
zipCode: string;
}
function isAddress(value: unknown): value is Address {
return (
typeof value === "object" &&
value !== null &&
"street" in value &&
"city" in value &&
"zipCode" in value &&
typeof (value as Address).street === "string" &&
typeof (value as Address).city === "string" &&
typeof (value as Address).zipCode === "string"
);
}
function assertIsAddress(value: unknown): asserts value is Address {
if (!isAddress(value)) {
throw new Error("Invalid address");
}
}
// ============================================
// PART 5: THE API RESPONSE VALIDATION
// ============================================
interface UserProfile {
id: number;
username: string;
addresses: Address[];
}
function assertIsUserProfile(value: unknown): asserts value is UserProfile {
if (typeof value !== "object" || value === null) {
throw new Error("User profile must be an object");
}
const obj = value as Record<string, unknown>;
if (typeof obj.id !== "number") {
throw new Error("User ID must be a number");
}
if (typeof obj.username !== "string") {
throw new Error("Username must be a string");
}
if (!Array.isArray(obj.addresses)) {
throw new Error("Addresses must be an array");
}
if (!obj.addresses.every(isAddress)) {
throw new Error("All addresses must be valid");
}
}
async function loadUserProfile(userId: string): Promise<UserProfile> {
const response = await fetch(`/api/users/${userId}/profile`);
const data: unknown = await response.json();
assertIsUserProfile(data);
return data; // data is UserProfile
}
// ============================================
// PART 6: THE TYPE PREDICATE COMPARISON
// ============================================
// Type predicate — returns boolean, used in if
function isNumber(value: unknown): value is number {
return typeof value === "number";
}
function processWithPredicate(value: unknown) {
if (isNumber(value)) {
console.log(value.toFixed(2));
} else {
console.log("Not a number");
}
}
// Assertion function — throws, narrows for rest of scope
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== "number") {
throw new Error("Not a number");
}
}
function processWithAssertion(value: unknown) {
assertIsNumber(value);
console.log(value.toFixed(2)); // no if block needed
}
// ============================================
// PART 7: THE ASSERTION VS TYPE ASSERTION TRAP
// ============================================
// ❌ Type assertion — no runtime safety
async function getUserBad(id: string) {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return data as UserProfile; // hope and pray
}
// ✅ Assertion function — runtime validated
async function getUserGood(id: string) {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
assertIsUserProfile(data);
return data; // verified at runtime
}
// ============================================
// PART 8: THE ASSERTION IN TEST CODE
// ============================================
// Vitest 4.0+ includes expect.assert
// which is typed as an assertion function
import { expect, test } from "vitest";
test("reads stored user", () => {
const cache = new Map<string, { id: string; name: string }>();
cache.set("alice", { id: "1", name: "Alice" });
const user = cache.get("alice"); // { id, name } | undefined
expect.assert(user); // throws if undefined, narrows below
expect(user.name).toBe("Alice"); // no ! or as needed
});
// ============================================
// PART 9: THE OVERLOADED ASSERTION FUNCTION
// ============================================
type Robot = RobotOn | RobotOff;
class RobotOn {
public readonly state = "on" as const;
public speak() { console.log("heyman!"); }
}
class RobotOff {
public readonly state = "off" as const;
}
function assertRobotState(robot: Robot, state: "on"): asserts robot is RobotOn;
function assertRobotState(robot: Robot, state: "off"): asserts robot is RobotOff;
function assertRobotState(robot: Robot, state: "on" | "off") {
if (robot.state !== state) {
throw new Error(`Robot is not ${state}`);
}
}
function maybeSpeak(robot: Robot) {
assertRobotState(robot, "on");
robot.speak(); // robot is RobotOn
}
// ============================================
// PART 10: THE ASSERTION FUNCTION CONTRACT
// ============================================
// The compiler trusts you. If you write an empty body,
// the assertion is a lie. TypeScript will not catch it.
function assertIsStringLie(value: unknown): asserts value is string {
// ❌ no throw, no check — but the signature claims otherwise
// The compiler allows it. The narrowing is unsound.
}
function dangerousUse(value: unknown) {
assertIsStringLie(value);
console.log(value.toUpperCase()); // compiles, but may crash
}
The ten parts cover the basic asserts condition, the type-specific assert, the generic non-null assert, the complex object assert, API response validation, the type predicate comparison, the type assertion trap, assertion functions in tests, overloaded assertion functions, and the contract caveat.
Quick Reference
The Assertion Forms
| Form | Meaning | Example |
|---|---|---|
asserts condition | Throws if condition is falsy | function assert(x: unknown): asserts x |
asserts value is Type | Throws if value is not Type | function assert(x: unknown): asserts x is string |
asserts value | Throws if value is falsy | function assert(x: unknown): asserts x |
Assertion Functions vs Type Predicates
| Feature | Type Predicate | Assertion Function |
|---|---|---|
| Return type | value is Type | asserts value is Type |
| Returns | boolean | void (throws on failure) |
| Usage | if (isX(v)) | assertX(v); |
| Narrowing scope | Inside if block | Rest of scope |
| Runtime behavior | Returns true/false | Throws on failure |
Assertion Functions vs Type Assertions
| Feature | Type Assertion (as) | Assertion Function |
|---|---|---|
| Runtime check | None | Full validation |
| Error location | Later (far away) | At the call site |
| Safety | Unsound | Sound (if written correctly) |
| Use case | Known types, lost info | External data, preconditions |
Common Assertion Helpers
| Helper | Signature | Purpose |
|---|---|---|
assertDefined | asserts value is T | Non-null, non-undefined |
assertIsString | asserts value is string | String validation |
assertIsNumber | asserts value is number | Number validation |
assertIsArray | asserts value is T[] | Array validation |
Best Practices
✅ Do This:
// Use assertion functions for shared preconditions
function assertDefined<T>(value: T | null | undefined, name: string): asserts value is T {
if (value == null) throw new Error(`${name} is required`);
}
// Layer type guards and assertion functions
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === "string");
}
function assertIsStringArray(value: unknown): asserts value is string[] {
if (!isStringArray(value)) throw new Error("Expected string array");
}
// Use assertion functions to validate external data
const data: unknown = await response.json();
assertIsUserProfile(data);
return data; // typed and verified
// Use overloaded assertion functions for discriminated unions
function assertRobotState(robot: Robot, state: "on"): asserts robot is RobotOn;
❌ Don’t Do This:
// Don't use as for external data
const user = data as User; // no runtime check
// Don't write an assertion function without throwing
function assertIsString(value: unknown): asserts value is string {
// no throw — this is a lie
}
// Don't use assertion functions when a type predicate suffices
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== "number") throw new Error();
}
// Use if (isNumber(value)) instead when handling both branches
// Don't use assertion functions for user-facing validation
// They throw. Return a Result or boolean for recoverable errors.
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Assertion doesn’t narrow | Missing asserts in return type | Add asserts value is T |
| Compiler accepts empty assert | TypeScript trusts the signature | Always throw on failure |
| Narrowing only in scope | Assertion is a statement | Call at the top of the function |
| Assertion function returns value | asserts functions return void | Use type predicate for boolean |
| Overloaded assert order wrong | Broad overload first | Specific overloads first |
Used as instead of assert | No runtime check | Use assertion function for external data |
Real-World Examples
1. Basic Assert
function assert(condition: unknown, msg?: string): asserts condition {
if (!condition) throw new Error(msg);
}
2. Non-Null Assertion
function assertDefined<T>(value: T | null | undefined, name: string): asserts value is T {
if (value == null) throw new Error(`${name} is required`);
}
3. String Validation
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") throw new Error("Not a string");
}
4. Array Validation
function assertIsArray(value: unknown): asserts value is unknown[] {
if (!Array.isArray(value)) throw new Error("Not an array");
}
5. Complex Object Validation
function assertIsUser(value: unknown): asserts value is User {
if (typeof value !== "object" || value === null) throw new Error("Not an object");
}
6. API Response Validation
const data: unknown = await response.json();
assertIsUserProfile(data);
return data;
7. Overloaded Assertion
function assertRobotState(robot: Robot, state: "on"): asserts robot is RobotOn;
8. Assertion in Tests (Vitest 4+)
expect.assert(user); // throws and narrows
9. Layered Guard + Assert
function isStringArray(value: unknown): value is string[] { ... }
function assertIsStringArray(value: unknown): asserts value is string[] {
if (!isStringArray(value)) throw new Error();
}
10. Type Predicate Alternative
function isNumber(value: unknown): value is number {
return typeof value === "number";
}
Visual: Assertion Function Flow
┌──────────────────────────────────────────────┐
│ ASSERTION FUNCTION FLOW │
│ │
│ function assertIsString(value: unknown) │
│ : asserts value is string { │
│ if (typeof value !== "string") { │
│ throw new Error("Not a string"); │
│ } │
│ } │
│ │
│ Caller: │
│ assertIsString(input); │
│ │ │
│ ▼ │
│ Did it throw? │
│ │ │ │
│ NO YES │
│ │ │ │
│ ▼ ▼ │
│ input is Error thrown │
│ string (program stops) │
│ │
│ No if-block needed. Narrowing applies │
│ for the rest of the scope. │
│ │
└──────────────────────────────────────────────┘
Visual: Assertion vs Type Predicate
┌──────────────────────────────────────────────┐
│ ASSERTION vs TYPE PREDICATE │
│ │
│ Type Predicate: │
│ if (isString(value)) { │
│ value.toUpperCase(); // narrowed here │
│ } else { │
│ value; // still unknown │
│ } │
│ │
│ Assertion Function: │
│ assertIsString(value); │
│ value.toUpperCase(); // narrowed below │
│ │
│ Predicate = boolean return, if-block │
│ Assertion = void return, throws, scope-wide │
│ │
└──────────────────────────────────────────────┘
Visual: Assertion vs Type Assertion
┌──────────────────────────────────────────────┐
│ ASSERTION vs TYPE ASSERTION │
│ │
│ Type Assertion (as): │
│ const user = data as User; │
│ └─ No runtime check │
│ └─ Compiler trusts you │
│ └─ Error appears later if wrong │
│ │
│ Assertion Function: │
│ assertIsUser(data); │
│ └─ Runtime validation │
│ └─ Throws at call site if wrong │
│ └─ Narrowing after call │
│ │
│ Use assertion functions for external data. │
│ │
└──────────────────────────────────────────────┘
Visual: Layered Guard and Assert
┌──────────────────────────────────────────────┐
│ LAYERED GUARD AND ASSERT │
│ │
│ Type Guard (reusable check): │
│ function isStringArray(v: unknown) │
│ : v is string[] { │
│ return Array.isArray(v) && │
│ v.every(i => typeof i === "string"); │
│ } │
│ │
│ Assertion (precondition): │
│ function assertIsStringArray(v: unknown) │
│ : asserts v is string[] { │
│ if (!isStringArray(v)) throw ...; │
│ } │
│ │
│ Guard = boolean, reusable in expressions │
│ Assert = throws, narrows the scope │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Assertion function | Validates and throws on failure |
| Return type | asserts condition or asserts value is Type |
| Runtime behavior | Throws if condition is false |
| Narrowing scope | Rest of the calling scope |
| vs Type predicate | Predicate returns boolean, assertion throws |
| vs Type assertion | as has no runtime check |
| Generic helper | assertDefined<T> for non-null |
| Test utility | Vitest expect.assert is an assertion function |
| Overloaded | Can assert discriminated union members |
Key takeaways:
- Assertion functions combine runtime validation with compile-time narrowing. The
assertskeyword in the return type tells TypeScript that a successful return means the condition is true. The function throws on failure, and the compiler narrows the type for the rest of the scope . - There are two forms:
asserts conditionandasserts value is Type. The first asserts a boolean expression. The second asserts that a specific value has a specific type. Both throw on failure and returnvoid. - Assertion functions differ from type predicates. A type predicate returns a boolean and is used in
if/else. An assertion function returnsvoid, throws on failure, and narrows the type unconditionally after the call . - Assertion functions differ from type assertions (
as). A type assertion performs no runtime check. An assertion function validates at runtime and throws immediately if the validation fails, giving a clear error at the point of failure . - Use assertion functions for shared preconditions and external data. They centralize validation logic and eliminate the repeated
if (x === null) throwpattern that otherwise appears in every function . - The compiler trusts you. An assertion function with an empty body or a missing throw will still narrow the type, but the narrowing is unsound. The compiler does not verify that you actually validate .
- Layer type guards and assertion functions. A type guard is a reusable boolean check. An assertion function can call the guard and throw if it returns false. This separates the validation logic from the control flow .
Remember: Assertion functions are a contract with the compiler. You promise that if the function returns, the condition is true. The compiler believes you and narrows the type accordingly. This makes them powerful for preconditions and external data validation, but also dangerous if written carelessly — the compiler will not catch an assertion that does not actually assert. Use them for the checks that must succeed for the rest of the function to make sense. Use type predicates when you need a boolean. Use as only when you have independent evidence the type is correct and the cost of a runtime check is prohibitive. The safest code validates at the boundary with assertion functions and then relies on the narrowed types throughout.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!