Skip to content
TypeScript ts narrowing 3 min read

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 function declarations (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.

AspectType guard (x is T)Assertion (asserts x is T)
Returnsbooleanvoid (throws on failure)
Control flowbranch with iflinear; throws otherwise
Narrowsinside the true branchrest of the scope
Use wheninvalid input is expected/handledinvalid 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 T to narrow to a type, or asserts x to 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 unknown data 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 assertX for clarity.
Last updated June 29, 2026
Was this helpful?