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

tigerspec

v2026.7.7

Published

**A runtime assertion library.**

Downloads

134

Readme

spec.js

A runtime assertion library.

Doesn't have coercion, serialization, compilation, masking, or timeouts.

Why

Executable Schema Reference

  • Document data model shapes in lieu of static types.
  • Catch and pinpoint breaking changes with actionable errors.
  • Prevent propagation of undefined or unexpected values early on.

System Invariant Checks (Correctness over uptime)

  • Fail-fast on invalid program states with runtime assertions.

References

Heavily inspired by TigerStyle and Clojure Spec

Walkthrough

This guide explains the design, API surface, and usage of the spec.js validation library.


1. Quick start

Define a schema to validate a configuration payload:

import { spec, t } from './spec.js';

const configSchema = spec({
  server: {
    host: t.string,
    port: t.number.min(1).max(65535),
  },
  database: t.optional({
    urls: [t.string],           // Repeating array of strings
    retries: t.number.min(0),   // Retries must be >= 0
  }),
});

Validate data using either enforcement (assert) or inspection (diff).

Enforcement (assert)

.assert(data) returns data on success, or throws a ValidationError (which inherits from TypeError) on failure:

const invalidConfig = {
  server: { host: 'localhost', port: 8080 },
  database: {
    urls: ['postgres://db1', 'postgres://db2'],
    retries: -5, // Invalid: must be >= 0
  },
};

try {
  configSchema.assert(invalidConfig);
} catch (error) {
  console.dir(error, { depth: null });
}

Specify a custom error message as a second argument:

configSchema.assert(invalidConfig, 'Invalid configuration');

Inspection (diff)

.diff(data) returns undefined on success, or a mismatch object detailing the failure on failure:

const mismatch = configSchema.diff(invalidConfig);

if (mismatch) {
  console.log(mismatch.kind);        // "min"
  console.log(mismatch.displayPath); // ".database.retries"
  console.log(mismatch.path);        // [".database", ".retries"]
}

2. Core concept: Everything is a spec

Validators are Spec instances. The spec() parser is idempotent, which allows composing schemas from reusable blocks:

const portSpec = t.number.min(1).max(65535);
const hostSpec = t.string.min(1);

const serverSchema = spec({
  host: hostSpec,
  port: portSpec,
});

spec.from(callback) provides access to the t object within the callback, avoiding explicit imports of t:

import { spec } from './spec.js';

const userSchema = spec.from((t) => ({
  name: t.string,
  age: t.number,
}));

3. Primitive types and literal constraints

The t namespace provides validators for JavaScript types and literal values.

Primitive types

Validators on t match standard JavaScript types:

  • t.string, t.number, t.bigint, t.boolean, t.symbol, t.null
  • t.array: Matches any array.
  • t.function: Matches executable functions.
  • t.object: Matches plain objects (rejects null and arrays).
t.string.assert("hello");
t.number.assert(42);
t.object.assert({ user: "Alice" });

t.object.diff(null);        // Fails
t.object.diff([1, 2, 3]);   // Fails

Literal constraints

Passing a primitive value to spec() creates a literal constraint checked with strict equality (===). Passing undefined directly throws a TypeError; use t.undefined to match undefined:

const exactStr = spec("hello");
exactStr.assert("hello"); // Succeeds

const exactNum = spec(7);
exactNum.assert(7);       // Succeeds
exactNum.diff(8);         // Fails (8 !== 7)

4. Custom predicates and range bounds

Attach custom logic constraints to any Spec instance.

Custom logic with .and()

Chain constraints using .and(schema). A function argument acts as a predicate that must return true to pass validation:

const idSpec = t.string.and((s) => s.startsWith("id-"));

idSpec.assert("id-1092"); // Succeeds
idSpec.diff("user-1092"); // Fails

Passing a function directly to spec() is a shorthand for a predicate constraint:

const evenNumber = spec((n) => n % 2 === 0);
evenNumber.assert(4); // Succeeds

Range checks using .min() and .max()

.min(limit) and .max(limit) apply numeric or length bounds:

  • Numbers and bigints: Compares the numeric value directly.
  • Strings: Compares the string length.
  • Arrays: Compares the array length.
  • Objects: Compares the number of keys.
  • Other types (undefined, null, function, symbol, boolean): Evaluates against NaN. Since comparison against NaN is always false, range constraints on these types evaluate to false (fail validation).
const scoreSpec = t.number.min(0).max(100);
scoreSpec.assert(50);

const usernameSpec = t.string.min(3).max(10);
usernameSpec.assert("alex");

const listSpec = t.array.min(1);
listSpec.assert(["item"]);

const objectWithKeys = t.object.min(2).max(4);
objectWithKeys.assert({ a: 1, b: 2 });

5. Complex collections

Objects and key validation

Object schemas validate defined properties. Validation is open by default: extra properties are preserved:

const userSchema = spec({
  name: t.string,
});

const payload = {
  name: "Alice",
  role: "Admin", // Preserved
};

const user = userSchema.assert(payload);
console.log(user.role); // "Admin"

To validate relationships between properties, apply a predicate to the parent object schema:

const rangeSchema = spec({
  minVal: t.number,
  maxVal: t.number,
}).and((data) => data.maxVal >= data.minVal);

rangeSchema.assert({ minVal: 10, maxVal: 20 }); // Succeeds
rangeSchema.diff({ minVal: 20, maxVal: 10 });   // Fails

Array validation styles

Array schemas support three validation styles:

  1. Empty Array []: Matches any array.
  2. Repeating Array [schema]: Validates that every element in the array matches the schema.
  3. Tuple [schema1, schema2, ...]: Validates positional elements. Tuples are strictly closed: the input array must match the exact length of the schema.
// 1. Empty Array: matches any array
const anyArray = spec([]);
anyArray.assert([1, "two", {}]);

// 2. Repeating Array: matches all items
const numbersArray = spec([t.number]);
numbersArray.assert([1, 2, 3]);
numbersArray.diff([1, "two"]); // Fails

// 3. Tuple: strictly closed positional array
const geoCoords = spec([t.number, t.number]);
geoCoords.assert([37.7749, -122.4194]);
geoCoords.diff([37.7749, -122.4194, "altitude"]); // Fails (length must be 2)

6. Handling absence and undefined values

The following helpers validate optional or missing data:

  • t.any(): Matches any value except undefined (equivalent to defined).
  • t.optional(schema): Matches undefined or the nested schema.
  • t.nullable(schema): Matches null or the nested schema.

These helpers can be nested:

const maybeMiddleName = t.optional(t.nullable(t.string));
maybeMiddleName.assert("Marie");   // Succeeds
maybeMiddleName.assert(null);      // Succeeds
maybeMiddleName.assert(undefined); // Succeeds

Handling undefined values

To protect against typos, spec() throws a TypeError if it parses a direct undefined value (such as { host: t.sting }):

// Throws: "Unsupported [undefined] while parsing schema."
spec({ host: t.sting }); 

Use t.undefined to explicitly validate undefined values or missing properties:

const schema = spec({
  middleName: t.undefined,
});

schema.assert({}); // Succeeds
schema.assert({ middleName: undefined }); // Succeeds

7. Union types

t.any(...schemas) validates that a value matches at least one of the schemas. Calling t.any() without arguments matches any value except undefined:

const stringOrNumberOrNull = t.any(t.string, t.number, t.null);

stringOrNumberOrNull.assert("hello"); // Succeeds
stringOrNumberOrNull.assert(42);      // Succeeds
stringOrNumberOrNull.assert(null);    // Succeeds
stringOrNumberOrNull.diff(undefined); // Fails

8. Error tagging and localization

The .tag(label) method associates custom metadata or labels with constraints.

Single tag fallback

Place a tag at the end of a chain to apply it to any failure within the chain:

const ageSchema = t.number
  .and((n) => n >= 18)
  .tag("Invalid registration age");

// Any failure triggers the tag:
console.log(ageSchema.diff("not-a-number").tag); // "Invalid registration age"
console.log(ageSchema.diff(16).tag);             // "Invalid registration age"

Granular tagging

Assign tags to specific constraints in a chain to return granular errors:

const ageSchema = t.number
  .tag("Age must be a number")
  .and((n) => n >= 18)
  .tag("You must be 18 or older to register");

// Fails the type check:
const typeErr = ageSchema.diff("underage");
console.log(typeErr.tag); // "Age must be a number"

// Fails the range check:
const rangeErr = ageSchema.diff(16);
console.log(rangeErr.tag); // "You must be 18 or older to register"

Tag propagation

A constraint uses the closest downstream tag in the chain:

const ageSchema = t.number
  .and(n => n >= 18)
  .tag('Must be a number, 18+')
  .and(n => n < 80); // Untagged constraint

// Fails t.number: uses closest downstream tag
console.log(ageSchema.diff('not-a-number').tag); // 'Must be a number, 18+'

// Fails n >= 18: uses closest downstream tag
console.log(ageSchema.diff(16).tag); // 'Must be a number, 18+'

// Fails n < 80: no tag downstream of this constraint
console.log(ageSchema.diff(90).tag); // undefined

Localization using symbols

Use Symbol values to resolve translation keys dynamically:

const ERR_NOT_A_NUMBER = Symbol("ERR_NOT_A_NUMBER");
const ERR_UNDERAGE = Symbol("ERR_UNDERAGE");

const ageSchema = t.number
  .tag(ERR_NOT_A_NUMBER)
  .and((n) => n >= 18)
  .tag(ERR_UNDERAGE);

const mismatch = ageSchema.diff(15);

const translations = {
  [ERR_NOT_A_NUMBER]: "Please enter a valid numeric value.",
  [ERR_UNDERAGE]: "You must be 18 or older to view this content.",
};

console.log(translations[mismatch.tag]); // "You must be 18 or older to view this content."

9. Validation errors and mismatch shapes

Validation errors

Validation failures in .assert() throw a ValidationError (which extends TypeError). It exposes these properties:

  • message: The custom message string passed to .assert(), the label string/symbol from a failing spec's .tag(), or "Assertion failed".
  • cause: The underlying mismatch object returned by .diff().
  • input: The original input value that failed validation.

Mismatch objects

.diff() returns a mismatch object on failure.

Common fields

Every mismatch object contains:

  • kind: A string matching the category of constraint failure ('literal', 'type', 'predicate', 'min', 'max', 'any').
  • data: The input value that failed validation.
  • tag: (Optional) The resolved tag label for the failing constraint.
  • path: (Optional) Array of path segments leading to the failure.
  • displayPath: (Optional) Formatted path string (for example, ".server.port" or "[0].name").

Constraint-specific fields

Other fields depend on the constraint kind:

| Failure kind | Description | expected Value | actual Value | | :--- | :--- | :--- | :--- | | 'literal' | Value did not match literal. | The expected literal value. | The actual input value. | | 'type' | Value was the incorrect type. | The expected type name string. | The actual type name string. | | 'predicate' | Custom predicate returned false. | String representation of the predicate function. | false | | 'min' | Value or length was below the minimum. | String representation of the limit number. | false | | 'max' | Value or length exceeded the maximum. | String representation of the limit number. | false | | 'any' | Value failed to match all schemas in union. | Array of mismatch objects from each sub-schema. | false |


10. Advanced validation examples

Recursive validation

Define recursive object schemas by wrapping validation in a lazy predicate:

let treeSpec;

const nodeSchema = {
  value: t.string,
  // Validate each child recursively using treeSpec.diff.
  children: [spec((node) => !treeSpec.diff(node))], 
};

treeSpec = spec(nodeSchema);

const validTree = {
  value: "root",
  children: [
    { value: "branch-a", children: [] },
    { value: "branch-b", children: [{ value: "leaf", children: [] }] }
  ]
};

treeSpec.assert(validTree); // Succeeds

Class instance validation

Validate instance types using instanceof inside a custom predicate:

const dateSpec = spec((val) => val instanceof Date);

dateSpec.assert(new Date()); // Succeeds
dateSpec.diff("2026-07-06");  // Fails