Skip to content

Custom assertions

assertcheck exposes its internal formatting primitives so you can build domain-specific assertions that match the library's output style exactly — same format, same structure, same developer experience.


When to write a custom assertion

The built-in assertions cover generic invariants. Write a custom assertion when you have a domain-specific invariant that appears in multiple places and deserves a named contract:

  • assertValidOrderId — "must be a non-empty string in ord_* format"
  • assertSupportedCurrency — "must be one of the ISO 4217 codes we accept"
  • assertValidEmail — "must match RFC 5322 format"
  • assertPositiveInteger — "must be an integer strictly greater than 0"

A named custom assertion communicates intent. assert.string(v) + assert.matches(v, /^ord_/) works, but assertValidOrderId(v) is self-documenting and produces a richer error.


Building a custom assertion

ts
import { buildBlock, color, fmtValue, fail, parseOpts } from "assertcheck"
import type { AssertOptions } from "assertcheck"

export function assertPositiveInteger(
  value: unknown,
  opts?: string | AssertOptions
): asserts value is number {
  if (typeof value === "number" && Number.isInteger(value) && value > 0) return

  const o = parseOpts(opts)
  fail({
    assertion: "positiveInteger",
    message: buildBlock({
      assertion: "positiveInteger",
      title: o.msg ?? "Expected a positive integer",
      rows: [
        { label: "expected", value: "integer > 0",   indicator: color.added("+") },
        { label: "received", value: fmtValue(value), indicator: color.removed("✗") },
      ],
      note: o.note,
    }),
    actual:   value,
    expected: "positive integer",
  })
}

The output matches the assertcheck style exactly:

══════════════════ ● Expected a positive integer ══════════════

── values ──────────────────────────────────────────────────────
  + expected        integer > 0
  ✗ received        -3

════════════════════════════════════════════════════════════════

A domain-specific example: order ID format

ts
import { buildBlock, color, fmtValue, fail, parseOpts } from "assertcheck"
import type { AssertOptions } from "assertcheck"

const ORDER_ID_PATTERN = /^ord_[a-z0-9]{16}$/

export function assertValidOrderId(
  value: unknown,
  opts?: string | AssertOptions
): asserts value is string {
  if (typeof value === "string" && ORDER_ID_PATTERN.test(value)) return

  const o = parseOpts(opts)
  fail({
    assertion: "validOrderId",
    message: buildBlock({
      assertion: "validOrderId",
      title: o.msg ?? "Invalid order ID format",
      rows: [
        { label: "expected format", value: "ord_<16 hex chars>", indicator: color.added("+") },
        { label: "received",        value: fmtValue(value),       indicator: color.removed("✗") },
      ],
      note: o.note ?? "Order IDs are generated by createOrder() — do not construct them manually.",
    }),
    actual:   value,
    expected: "valid order ID",
  })
}

Usage:

ts
assertValidOrderId(orderId, {
  msg:  "orderId must be a valid order ID",
  note: "check that the ID comes from the orders service",
})

Available formatting primitives

ExportPurpose
buildBlock(def)Build the full formatted error string from a BlockDef
fmtValue(v)Format any value for display — handles nesting, truncation, type labels
colorANSI colour helpers: color.added, color.removed, color.index, etc.
parseOpts(opts)Normalise string | AssertOptions | undefined{ msg, note, actual }
fail(opts)Output the formatted error and throw AssertionError
diffObjects(a, b)Generate diff rows between two objects (for deep equality assertions)

Negation: assert.not instead of assertNotX

Rather than writing a separate assertNotPositiveInteger, use assert.not:

ts
assert.not(assertPositiveInteger, value)
// passes if assertPositiveInteger would throw — i.e. value is NOT a positive integer

This works with any assertion function — built-in or custom.


Composing with built-in assertions

Custom assertions can call built-in ones internally. This is the cleanest way to build layered contracts:

ts
export function assertValidUser(value: unknown): asserts value is User {
  assert.object(value, "user must be an object")
  assert.hasKeys(value as object, ["id", "email", "role"], "user is missing required fields")
  assertValidEmail((value as any).email, {
    msg:  "user.email must be a valid email address",
    note: "check the registration form validation",
  })
}

Each layer adds domain-specific meaning while reusing the built-in primitives for low-level checks.

Released under the Apache 2.0 License. Built by Vagabond Studio — senior-only for growing companies.