npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

assertcheck

v0.6.0

Published

Negative Space Programming for TypeScript — declare invalid states, fail fast, trust the boundary.

Readme

AssertCheck

Negative Space Programming for TypeScript. Declare what cannot exist. Fail where it matters. Ship with confidence.

npm JSR License TypeScript Bun Documentation


The problem with "defensive" code

Most TypeScript codebases hide bugs behind if (!x) return. Silent failures. No trace. No context. Hours lost in production.

// Before — bad data propagates in silence
function chargeOrder(order: Order) {
  if (!order || !order.amount) return // swallowed, never caught, never debugged
}
// After — invalid state is declared at the boundary
function chargeOrder(order: Order) {
  assert.notNil(order, "order is required")
  assert.positive(order.amount, "order amount must be positive")
  assert.equal(order.status, "pending", {
    msg:    "order must be pending before charge",
    actual: "order.status",
    note:   "call resetOrder() before retrying",
  })
  // from here: every assumption is verified, every invariant is explicit
}

AssertCheck makes invalid states impossible to ignore. Not a validator. Not a schema library. A contract system — at every boundary, for every assumption.

Assertions are always enabled. There is no mode system, no NODE_ENV switch, no configuration. Every failure prints a formatted diagnostic and throws an AssertionError.


Why AssertCheck?

| | if/return | node:assert | zod / yup | AssertCheck | |---|:---:|:---:|:---:|:---:| | Fails loudly | No | Yes | Yes | Yes | | Type narrowing | No | Partial | Yes | Yes | | Structured, readable errors | No | Partial | Partial | Yes | | Structural diff on deep equality | No | Partial | No | Yes | | Chainable fluent API | No | No | No | Yes | | Async assertions (rejects / resolves) | No | Yes | No | Yes | | Function properties (purity, idempotence) | No | No | No | Yes | | AI skills included | No | No | No | Yes |

Zod and Yup validate data crossing the outside boundary (forms, APIs, JSON). AssertCheck enforces invariants at every internal boundary: function arguments, state transitions, external responses, collection shapes. They complement each other.


Install

# npm / yarn / pnpm / bun
npm install assertcheck
yarn add assertcheck
pnpm add assertcheck
bun add assertcheck

# JSR (Deno / Bun / npm)
deno add jsr:@thonymg/assertcheck
bunx jsr add @thonymg/assertcheck
npx jsr add @thonymg/assertcheck

Requirements: Node.js 18+, Bun 1+, or Deno 1.38+. TypeScript 5+. lodash is a regular dependency and is installed automatically.

When installed from JSR, import from @thonymg/assertcheck instead of assertcheck.


Quick start

Single assertions

import { assert } from "assertcheck"

assert.notNil(user, "user is required")
assert.equal(order.status, "pending", {
  msg:    "order must be pending before payment",
  actual: "order.status",
  note:   "call resetOrder() first",
})

Every assertion takes an optional last argument: a plain string (the message) or an options object with msg, actual (label for the received value) and note (what the caller should do).

Fluent chains

import { check } from "assertcheck"

check(users)
  .notEmpty("users list cannot be empty")
  .noNils("no null users allowed")
  .uniqueBy("id", "duplicate user IDs detected")
  .all(u => u.active, "all users must be active")
  .sortedBy("createdAt")

Async

const user = await assert.resolvesNotNil(db.users.findById(id), "user must exist")

await assert.rejects(() => api.charge(expiredCard), PaymentError, "expired card must be rejected")
await assert.rejectsMatching(api.pay(order), /insufficient funds/)

Async assertions accept a Promise or a zero-arg thunk () => Promise. Use the thunk when the callee may throw synchronously before returning a promise.


Error output that actually helps

When an assertion fires, you get a precise, ELM-inspired diagnostic — not a 40-line stack trace.

══════════════════ ● order must be pending before charge ══════

── values ──────────────────────────────────────────────────────
  + expected        "pending"
  ✗ order.status    "paid"

── note ────────────────────────────────────────────────────────
  call resetOrder() before retrying

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

Deep equality failures include a structural diff, field by field:

══════════════════ ● Deep equality failed ═════════════════════

── diff ────────────────────────────────────────────────────────
  ·  id           "usr_123"
  ~  status
       expected   "active"
       actual     "banned"
  +  role         "admin"   ← missing

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

Output adapts automatically:

  • Node / Bun / Deno — ANSI colours on a TTY, plain text in pipes and CI. Respects NO_COLOR.
  • Browser — collapsible console.groupCollapsed in DevTools.

The stack trace is trimmed so the first frame points at your call site, not inside the library.


Full API

Every function is available as assert.<name>(...). The last parameter is always opts?: string | AssertOptions.

Existence

| | | |---|---| | nil(v) | Must be null or undefined | | notNil(v) | Must not be null or undefined — narrows to NonNullable<T> | | empty(v) | Must be empty (string / array / object / Map / Set) | | notEmpty(v) | Must not be empty — narrows to NonNullable<T> |

Type guards

| | Narrows to | |---|---| | string(v) | string | | number(v) | number | | integer(v) | number (integer) | | finite(v) | number (finite) | | boolean(v) | boolean | | array<T>(v) | T[] | | object<T>(v) | T | | func<T>(v) | T | | instanceOf(v, Ctor) | instance of Ctor |

Equality

| | | |---|---| | equal(actual, expected) | Strict === | | deepEqual(actual, expected) | Deep equality with structural diff |

Numerics

positive(n) · negative(n) · zero(n) · greater(a, b) · greaterOrEqual(a, b) · less(a, b) · lessOrEqual(a, b) · withinRange(v, min, max) · inDelta(actual, expected, delta)

Arrays (Ruby-inspired)

| | | |---|---| | len(arr, n) · longerThan(arr, n) · shorterThan(arr, n) | Length | | includes(arr, item) · containsAll(arr, items) · containsNone(arr, items) · subset(arr, sub) | Membership | | all(arr, pred) · any(arr, pred) · none(arr, pred) · one(arr, pred) · count(arr, pred, n) | Predicates — all narrows with a type guard | | elementsMatch(a, b) | Same elements, any order | | unique(arr) · uniqueBy(arr, key) | Uniqueness | | increasing(arr) · nonDecreasing(arr) · sortedBy(arr, key) | Ordering | | first(arr, expected) · last(arr, expected) | Boundary elements | | sumBy(arr, key, expected) | Aggregation | | noNils(arr) · flat(arr) · allInstanceOf(arr, Ctor) | Shape — noNils and allInstanceOf narrow | | zippedWith(a, b, pred) · groupedBy(arr, key, groups) · partition(arr, pred, nMatch, nRest) | Structure |

Objects (Ruby Hash-inspired)

| | | |---|---| | hasKey(obj, key) · hasKeys(obj, keys) | Required keys — narrow to T & Record<K, unknown> | | hasExactKeys(obj, keys) · hasOnlyKeys(obj, allowed) | Exact / allowed key set | | hasValue(obj, key, expected) | Key holds the expected value | | containsSubset(obj, subset) | Partial deep match | | allValuesMatch(obj, pred) · noNilValues(obj) | Value constraints | | dig(obj, "a.b.c", expected) | Nested path equals expected value |

Functions (mathematical properties)

| | | |---|---| | returns(fn, args, expected) | fn(...args) deep-equals expected | | pure(fn, args) | Same output on repeated calls, arguments untouched | | idempotent(fn, arg) | fn(fn(x)) === fn(x) | | arity(fn, n) | Declared parameter count | | mapsDistinct(fn, inputs) | Distinct inputs give distinct outputs | | homomorphic(fn, a, b, op) | fn(op(a, b)) === op(fn(a), fn(b)) |

Async (Promise or thunk)

| | | |---|---| | rejects(p) · rejects(p, ErrorCtor) | Rejects, optionally with an instance of ErrorCtor | | rejectsWithMessage(p, msg) | Rejection message equals msg | | rejectsMatching(p, /re/) | Rejection message matches the pattern | | rejectsSatisfying(p, pred) | Rejection value satisfies the predicate | | resolves(p) | Resolves — returns the value | | resolvesWith(p, expected) | Resolves to a deep-equal value | | resolvesSatisfying(p, pred) | Resolved value satisfies the predicate | | resolvesNotNil(p) | Resolves to a non-nil value — returns NonNullable<T> |

Negation

assert.not(fn, ...args) is the only negation API. It passes when the wrapped assertion would throw, and works with built-in and custom assertions alike.

assert.not(assert.equal, user.role, "admin")
assert.not(assert.includes, errors, "FATAL")
assert.not(assert.hasKey, patch, "id")

Chainable API

check(value) returns an ArrayChecker for arrays, an ObjectChecker for plain objects, and a bare Checker for anything else. Each method calls the matching assert.* and returns this, so the chain stops at the first failure.

import { check } from "assertcheck"

// Arrays
check(users)
  .notEmpty()
  .noNils()            // narrows the rest of the chain to NonNullable<T>[]
  .uniqueBy("id")
  .all(u => u.active)
  .sortedBy("createdAt")
  .len(10)

// Objects
check(config)
  .hasKeys(["host", "port"])
  .noNilValues()
  .dig("database.pool.max", 10)

// Any value — tap for side effects mid-chain
check(orders)
  .tap(v => console.log("orders:", v.length))
  .all(o => o.status === "paid")

Array methods: notEmpty · len · longerThan · shorterThan · includes · all · any · none · one · count · unique · uniqueBy · noNils · sortedBy · increasing · nonDecreasing · first · last · sumBy · subset · elementsMatch · containsAll · containsNone · flat · groupedBy · allInstanceOf · zippedWith · partition

Object methods: notEmpty · hasKey · hasKeys · hasExactKeys · hasOnlyKeys · deepEqual · containsSubset · noNilValues · allValuesMatch · dig


AssertionError

import { assert, AssertionError } from "assertcheck"

try {
  assert.equal(order.status, "pending")
} catch (err) {
  if (err instanceof AssertionError) {
    err.assertion // "equal"
    err.actual    // "paid"
    err.expected  // "pending"
    err.message   // the full formatted block
  }
}

Custom assertions

The formatting primitives are exported so domain assertions look native: buildBlock, fmtValue, diffObjects, color, output, parseOpts.

import { AssertionError, buildBlock, color, fmtValue, output, parseOpts } from "assertcheck"
import type { AssertOptions } from "assertcheck"

export function assertOrderId(v: unknown, opts?: string | AssertOptions): asserts v is string {
  if (typeof v === "string" && /^ord_[a-z0-9]{16}$/.test(v)) return
  const o = parseOpts(opts)
  const message = buildBlock({
    assertion: "orderId",
    title: o.msg ?? "Invalid order ID",
    rows: [
      { label: "expected", value: "ord_<16 chars>", indicator: color.added("+") },
      { label: o.actual ?? "received", value: fmtValue(v), indicator: color.removed("✗") },
    ],
    note: o.note,
  })
  output(message)
  throw new AssertionError({ assertion: "orderId", message, actual: v, expected: "order ID" })
}

See the custom assertions guide for the full walkthrough.


AI Skills

AssertCheck ships with AI skills so your coding assistant applies the library's patterns for you.

| Skill | What it does | |---|---| | assertcheck-spec | Writes the precondition / postcondition table before any implementation | | assertcheck-feature | Builds a new function or service with its guard block first | | assertcheck-audit | Scans existing code for unguarded boundaries and proposes assertions | | assertcheck-refactor | Modifies code without silently dropping a safety check | | assertcheck-selector | Picks the most specific assert.* for a given invariant |

# Install all skills in the current project
bunx skills add thonymg/assertcheck --skill='*'
npx skills add thonymg/assertcheck --skill='*'

# Or globally
bunx skills add thonymg/assertcheck --skill='*' -g

Learn more at vercel-labs/skills.


Project structure

src/
  index.ts      — public barrel export
  types.ts      — AssertOptions, AssertionErrorOptions, BlockDef, RowDef
  env.ts        — runtime detection (Node / Bun / Deno / browser, NO_COLOR)
  error.ts      — AssertionError
  format.ts     — block builder, value formatter, diff engine, ANSI / DevTools output
  fail.ts       — internal: output + throw
  assert.ts     — every assertion, sync and async
  checker.ts    — check(), Checker, ArrayChecker, ObjectChecker
tests/
  assert.test.ts       — sync assertions
  assert.deep.test.ts  — deepEqual and diff output
  async.test.ts        — rejects / resolves family
examples/
  ecommerce/    — services and tests using assertcheck end to end
skills/         — AI skills (one folder per skill, SKILL.md + references)
docs-src/       — VitePress site (guide, skills, generated API reference)

Development

bun install
bun test               # test suite against src/
bun run test:dist      # same suite against the built dist/
bun run typecheck      # tsc --noEmit
bun run format         # prettier
bun run build          # bundle to ./dist
bun run docs:dev       # regenerate API docs (typedoc) and serve the site
bun run release        # interactive version bump, tag and publish (npm + JSR)

Pushing a v* tag triggers the publish workflow in .github/workflows/publish.yml.


License

Apache License 2.0 — free to use, modify, and distribute commercially as long as you retain attribution. See LICENSE.


Built by Vagabond Studio

Vagabond Studio is a fully remote, senior-only collective of engineers and designers. We build TypeScript, Vue.js, Rails, and Django products from greenfield to production — and we stay until it ships right.

AssertCheck is one of the open-source tools we maintain as a demonstration of how we approach software: explicit contracts, zero defensive noise, and code that communicates intent at every boundary.

What we do:

  • Product engineering — TypeScript, Vue.js, Rails, Django. Full-stack from greenfield to production, or embedded in your existing codebase.
  • UI/UX design — Interfaces designed and engineered in the same team. No handoffs. No agency bloat.
  • Technical leadership — Architecture decisions, code reviews, and the kind of senior judgment that prevents six-month rewrites.

Who we work with:

Growing companies between 5 and 200 people who need craft-level output without building a full in-house team. Startups shipping their first real product. Scale-ups that have outgrown their MVP and need the codebase to match their ambitions.

Book a discovery call — 30 minutes, no pitch, just a real conversation about your project.

Or reach us directly: [email protected]

We take on 2–3 new clients per quarter.