TypeScriptAssertion Functions (asserts)

Assertion Functions (asserts)

Assertion functions are a TypeScript feature that narrows types by throwing an error if a condition is not met. Unlike type predicates (which return true/false), assertion functions either throw or do nothing — and TypeScript narrows the type after the call because the function is guaranteed not to return if the condition fails.

The return type is written as asserts condition or asserts param is Type.

The Two Forms of Assertion Functions

Form

Syntax

When to use

Assert truthy

asserts condition

Assert any expression is truthy

Assert type

asserts param is Type

Assert a parameter is a specific type

Form 1: asserts condition

The simplest form asserts that a condition is truthy. TypeScript narrowing the types of all variables involved in the expression after the call.

TS
function assert(condition: unknown, message?: string): asserts condition {
  if (!condition) {
    throw new Error(message ?? 'Assertion failed')
  }
}

function processUser(user: User | null) {
  assert(user !== null, 'User must not be null')
  // After this line, TypeScript knows: user is User (not null)
  console.log(user.name) // safe — no optional chaining needed
  console.log(user.email.toLowerCase()) // also safe
}

interface User {
  name: string
  email: string
}
Note
The key insight: because assert throws if the condition is false, any code that runs after the call is guaranteed to be in the "condition was true" branch. TypeScript understands this and narrows accordingly.
Form 2: asserts param is Type

The second form asserts that a specific parameter is a certain type after the function runs. This is the assertion equivalent of a type predicate.

TS
function assertIsString(val: unknown): asserts val is string {
  if (typeof val !== 'string') {
    throw new TypeError(`Expected string, got ${typeof val}`)
  }
}

function assertIsNumber(val: unknown): asserts val is number {
  if (typeof val !== 'number') {
    throw new TypeError(`Expected number, got ${typeof val}`)
  }
}

function processValue(val: unknown) {
  assertIsString(val)
  // TypeScript knows: val is string here
  console.log(val.toUpperCase()) // safe!
}

// Parsing raw config data
function parseConfig(data: unknown) {
  const config = data as Record<string, unknown>
  assertIsString(config.host)
  assertIsNumber(config.port)
  // After assertions, both are narrowed:
  return { host: config.host, port: config.port }
  // host: string, port: number
}
Assertion Functions vs Type Predicates

Both assertion functions and type predicates narrow types, but they're used differently:

Feature

Type Predicate (is)

Assertion Function (asserts)

Returns

boolean (true/false)

void (or throws)

Narrowing happens

Inside if branch

After the call

On failure

Returns false

Throws an error

Use in conditions

Yes: if (isUser(x))

No: cannot use in if condition

Use in .filter()

Yes

No

Best for

Conditional branching

Invariant checking, preconditions

Real-World: Validating Environment Variables

A perfect use case for assertion functions: ensuring required environment variables exist before your app starts. The assertion runs at startup and throws immediately if something is missing, rather than failing mysteriously later.

TS
function assertEnvVar(name: string): asserts name is string {
  // This just narrows — but the real check is below
  void name
}

function requireEnv(name: string): string {
  const value = process.env[name]
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`)
  }
  return value
}

// Validate all required env vars at startup
const config = {
  databaseUrl: requireEnv('DATABASE_URL'),
  apiKey: requireEnv('API_KEY'),
  port: parseInt(requireEnv('PORT'), 10),
}

// From this point on, all values are strings (not undefined)
console.log(`Connecting to ${config.databaseUrl}`)
Real-World: DOM Element Assertions

TS
function assertElement<T extends Element>(
  selector: string,
  type: new (...args: any[]) => T,
): asserts type is new (...args: any[]) => T {
  void type // placeholder
}

// Simpler, more practical version:
function getElement<T extends Element>(
  selector: string,
  ElementType: new (...args: any[]) => T,
): T {
  const el = document.querySelector(selector)
  if (!(el instanceof ElementType)) {
    throw new Error(`Element "${selector}" not found or wrong type`)
  }
  return el
}

// Usage:
const form = getElement('#login-form', HTMLFormElement)
// form is HTMLFormElement — not HTMLElement | null
form.addEventListener('submit', (e) => {
  e.preventDefault()
  const data = new FormData(form) // TypeScript knows it's HTMLFormElement
})
Assertion Functions in Class Constructors

Assertion functions are useful for validating constructor arguments, throwing immediately with a clear error message rather than silently creating broken objects.

TS
function assertPositive(value: number, name: string): asserts value is number {
  if (value <= 0) {
    throw new RangeError(`${name} must be positive, got ${value}`)
  }
}

function assertNonEmpty(str: string, name: string): asserts str is string {
  if (str.trim().length === 0) {
    throw new Error(`${name} must not be empty`)
  }
}

class Product {
  readonly id: string
  readonly name: string
  readonly price: number

  constructor(id: string, name: string, price: number) {
    assertNonEmpty(id, 'Product ID')
    assertNonEmpty(name, 'Product name')
    assertPositive(price, 'Product price')

    this.id = id
    this.name = name
    this.price = price
  }
}

// Throws: "Product price must be positive, got -5"
// new Product('abc', 'Widget', -5)
Never-Reaching Assertion

A special assertion function that always throws is useful for exhaustiveness checking. If TypeScript determines a code path is unreachable, it should never be called.

TS
function assertNever(value: never, message?: string): never {
  throw new Error(message ?? `Unexpected value: ${JSON.stringify(value)}`)
}

type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number }

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'square':
      return shape.side ** 2
    default:
      // If you add a new Shape variant and forget to handle it,
      // TypeScript errors HERE at compile time
      return assertNever(shape)
  }
}
Warning
Assertion functions must have an explicit return type annotation — TypeScript does not infer asserts return types. Without the annotation, the function is treated as returning void and the narrowing won't work.
Combining Assertion with Zod or Similar

TS
// Using Zod for runtime validation with assertion-style API
// (Zod's .parse() throws on invalid — same semantic as assertions)

// Pure TypeScript equivalent without Zod:
interface ApiResponse {
  users: { id: number; name: string }[]
  total: number
}

function assertIsApiResponse(data: unknown): asserts data is ApiResponse {
  if (
    typeof data !== 'object' ||
    data === null ||
    !Array.isArray((data as any).users) ||
    typeof (data as any).total !== 'number'
  ) {
    throw new Error('Invalid API response shape')
  }
}

async function getUsers(): Promise<ApiResponse> {
  const res = await fetch('/api/users')
  const data: unknown = await res.json()

  assertIsApiResponse(data)
  // data is ApiResponse from here on
  return data
}
Success
Assertion functions are the "fail fast" tool in TypeScript's type narrowing toolkit. They're ideal for validating preconditions at function boundaries — ensuring your code crashes loudly at the source of a problem rather than silently propagating bad data.