| |

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

FormMeaningExample
asserts conditionThrows if condition is falsyfunction assert(x: unknown): asserts x
asserts value is TypeThrows if value is not Typefunction assert(x: unknown): asserts x is string
asserts valueThrows if value is falsyfunction assert(x: unknown): asserts x

Assertion Functions vs Type Predicates

FeatureType PredicateAssertion Function
Return typevalue is Typeasserts value is Type
Returnsbooleanvoid (throws on failure)
Usageif (isX(v))assertX(v);
Narrowing scopeInside if blockRest of scope
Runtime behaviorReturns true/falseThrows on failure

Assertion Functions vs Type Assertions

FeatureType Assertion (as)Assertion Function
Runtime checkNoneFull validation
Error locationLater (far away)At the call site
SafetyUnsoundSound (if written correctly)
Use caseKnown types, lost infoExternal data, preconditions

Common Assertion Helpers

HelperSignaturePurpose
assertDefinedasserts value is TNon-null, non-undefined
assertIsStringasserts value is stringString validation
assertIsNumberasserts value is numberNumber validation
assertIsArrayasserts 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

PitfallWhy It HappensFix
Assertion doesn’t narrowMissing asserts in return typeAdd asserts value is T
Compiler accepts empty assertTypeScript trusts the signatureAlways throw on failure
Narrowing only in scopeAssertion is a statementCall at the top of the function
Assertion function returns valueasserts functions return voidUse type predicate for boolean
Overloaded assert order wrongBroad overload firstSpecific overloads first
Used as instead of assertNo runtime checkUse 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

ItemValue
Assertion functionValidates and throws on failure
Return typeasserts condition or asserts value is Type
Runtime behaviorThrows if condition is false
Narrowing scopeRest of the calling scope
vs Type predicatePredicate returns boolean, assertion throws
vs Type assertionas has no runtime check
Generic helperassertDefined<T> for non-null
Test utilityVitest expect.assert is an assertion function
OverloadedCan assert discriminated union members

Key takeaways:

  • Assertion functions combine runtime validation with compile-time narrowing. The asserts keyword 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 condition and asserts 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 return void .
  • Assertion functions differ from type predicates. A type predicate returns a boolean and is used in if/else. An assertion function returns void, 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) throw pattern 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!