# Walkthrough

This guide teaches the current API through examples. Start with function contracts, then use the later sections as a reference for composition and errors.

---

## 1. Function contracts

Use specs to describe what an internal function requires and what it promises to return:

```javascript
import { spec } from 'spekvet'; // Or './spec.js' if you copy the module

const count = spec.number.and(Number.isSafeInteger).min(0);
const capacityInput = spec({
  capacity: count,
  used: count,
}).and(({ capacity, used }) => used <= capacity);
const capacityResult = spec({ remaining: count });

function remainingCapacity(input) {
  capacityInput.assert(input, 'Invalid capacity state'); // Precondition
  const result = { remaining: input.capacity - input.used };
  return capacityResult.assert(result, 'Invalid remaining capacity'); // Postcondition
}

remainingCapacity({ capacity: 10, used: 3 }); // { remaining: 7 }
```

The input spec checks both field types and their relationship. The result spec checks that the remaining capacity is a nonnegative safe integer. Define reusable specs outside the function so they are constructed once.

### Enforcement (`assert`)

`.assert(data, reason)` returns the same `data` on success. A failed precondition or postcondition throws an `Error`, using the optional reason as its message:

```javascript
remainingCapacity({ capacity: 10, used: 12 }); // Throws: "Invalid capacity state"
```

Let a broken internal contract reach the application's error boundary so the affected operation stops. The [error section](#9-validation-errors-and-mismatch-shapes) shows how to inspect the thrown error.

### Inspection (`diff`)

`.diff(data)` returns `undefined` on success or a mismatch object on failure. Use it when the caller needs to handle rejection, such as checking submitted input before invoking an internal function:

```javascript
const mismatch = capacityInput.diff({ capacity: 10, used: '3' });

if (mismatch) {
  console.log(mismatch.kind);        // "type"
  console.log(mismatch.path); // ["used"]
}
```

Both methods report validation failures from the same rules. Exceptions raised by custom predicates still propagate.

---

## 2. Core concept: Everything is a spec

These terms describe different parts of validation:

| Term | Meaning |
| --- | --- |
| Schema | A description accepted by `spec()`: a shape, literal, predicate, or existing `Spec`. |
| Spec | An executable validator with `.diff()`, `.assert()`, and methods for composing rules. |
| Predicate | A synchronous function whose truthy result passes validation and whose falsy result fails it. |
| Constraint | An internal validation function that returns `undefined` on success or a mismatch object on failure. |

Validators are [Spec](../spec.js#L8) instances. `spec(existingSpec)` returns that same instance, so reusable specs can be nested directly in schemas:

```javascript
const portSpec = spec.number.and(Number.isInteger).min(1).max(65535);
const hostSpec = spec.string.min(1);

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

### API at a glance

| Constructor or helper | Behavior |
| --- | --- |
| `spec(schema)` | Builds a spec, or returns an existing one unchanged. A function schema is a predicate. |
| `spec.string`, `spec.number`, etc. | Reusable type specs; the next section lists them. |
| `spec.literal(value)` | Matches a value with SameValueZero equality, including object or function identity. |
| `spec.lazy(resolve)` | Builds a spec whose schema is resolved, parsed, and cached on first validation. |
| `spec.size(expected)` | Builds a standalone exact value or size constraint using the measurement rules in section 4. |
| `spec.min(limit)`, `spec.max(limit)` | Builds a standalone bound using the same measurement rules. |
| `spec.any(...schemas)` | Logical OR: matches any supplied schema; without arguments, matches any defined value. |
| `spec.optional(schema)`, `spec.nullable(schema)` | Allows `undefined` or `null`, respectively, as an alternative to the schema. |

Every `Spec` has `.diff(data)` and `.assert(data, reason)`. Its `.and(schema)`, `.size(expected)`, `.min(limit)`, `.max(limit)`, and `.tag(label)` methods return new specs without changing the receiver. `.and(schema)` appends the supplied schema's requirements after the existing requirements; validation stops at the first mismatch. `spec.any(...schemas)` accepts values satisfying any one of the supplied requirements and can be thought of as the logical OR counterpart to `.and()`.

The module exports these names:

| Export | Role |
| --- | --- |
| `spec` | The callable validation API described in this guide. |
| `internals` | Experimental access to implementation helpers, including `Spec` and `kindOf`. |

Compatibility for implementation details exposed through `internals` is not promised.

---

## 3. Primitive types and literal constraints

The `spec` namespace provides validators for JavaScript types and literal values.

### Primitive types

Validators on `spec` match useful JavaScript value categories:

* `spec.string`, `spec.number`, `spec.bigint`, `spec.boolean`, `spec.symbol`, `spec.null`, `spec.undefined`
* `spec.array`: Matches any array.
* `spec.function`: Matches executable functions.
* `spec.object`: Matches non-null objects except arrays; it does not check the prototype. Object shapes are intended for ordinary JSON-like data, although the type validator also accepts values such as `Date` and `Map`.

```javascript
spec.string.assert("hello");
spec.number.assert(42);
spec.object.assert({ user: "Alice" });

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

`spec.number` accepts finite primitive JavaScript numbers. It rejects `NaN`, `Infinity`, and `-Infinity`; use `spec.number.and(Number.isSafeInteger)` when an integer within JavaScript's exactly representable range is required. Numeric bounds alone do not require an integer.

Its mismatch describes the accepted value domain rather than merely repeating the API property name:

```javascript
spec.number.diff(NaN);
// { kind: "type", expected: "finite number", input: NaN }
```

Non-finite values are available explicitly when they are part of a domain:

```javascript
const numberOrPositiveInfinity = spec.any(spec.number, Infinity);
const numberOrEitherInfinity = spec.any(spec.number, Infinity, -Infinity);
const numberOrNaN = spec.any(spec.number, NaN);

numberOrPositiveInfinity.assert(Infinity);
numberOrEitherInfinity.assert(-Infinity);
numberOrNaN.assert(NaN);
```

`Infinity`, `-Infinity`, and `NaN` work as literal schemas. To accept every primitive value in JavaScript's `number` type, including all three non-finite values, use an explicit predicate:

```javascript
const anyJavaScriptNumber = spec(
  value => typeof value === "number"
);
```

Although JSON has no `NaN` or infinity literals, a valid number can overflow when parsed: `JSON.parse("1e400")` produces `Infinity`. Consequently, `spec.number` also provides a finite-number boundary for parsed JSON.

### Literal constraints

Passing a primitive value to `spec()` creates a literal constraint checked with SameValueZero equality. This behaves like strict equality (`===`), except that `NaN` matches itself. Passing `undefined` directly throws a `TypeError`; use `spec.undefined` to match `undefined`:

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

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

Use `spec.literal(value)` when you want literal matching explicitly. For objects, arrays, and functions, this compares identity instead of interpreting the value as a shape or predicate. `NaN` matches `NaN`, and `0` and `-0` compare equal.

---

## 4. Custom predicates and range bounds

Attach custom logic constraints to any [Spec](../spec.js#L8) instance.

### Custom logic with `.and()`

Chain constraints using `.and(schema)`. A function argument acts as a synchronous predicate: any truthy return value passes and any falsy return value fails. Async predicates are unsupported; returned Promises are not awaited and are treated as truthy. Exceptions thrown by predicates propagate to the caller.

```javascript
const idSpec = spec.string.and((s) => s.startsWith("id-"));

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

Passing a function directly to `spec()` creates a predicate spec:

```javascript
const even = spec((n) => n % 2 === 0);
const evenNumber = spec.number.and(even);
evenNumber.assert(4); // Succeeds
```

A predicate spec adds no type check of its own. Chain a type spec first when the predicate assumes a particular input type, as above.

Predicates run synchronously and can perform unbounded work. Put cheap limits before costly predicates when validating untrusted data; see the [bounded-predicate recipe](recipes.md#bounding-work-before-a-predicate).

### Size and range checks using `.size()`, `.min()`, and `.max()`

`.size(expected)` requires the measured value or size to equal `expected`. `.min(limit)` and `.max(limit)` apply inclusive bounds to the same measurement: it must be `>= limit` or `<= limit`, respectively.

* **Numbers and bigints**: Compares the numeric value directly.
* **Strings**: Compares JavaScript `.length` (UTF-16 code units).
* **Arrays**: Compares the array length.
* **Objects**: Compares the number of own enumerable string keys (`Object.keys(value).length`).
* **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).

```javascript
const scoreSpec = spec.number.min(0).max(100);
scoreSpec.assert(50);

const answerSpec = spec.number.size(42);
answerSpec.assert(42);

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

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

const pairSpec = spec.array.size(2);
pairSpec.assert(["left", "right"]);

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

When chained after `spec.number`, non-finite inputs fail the number constraint before reaching a size or bound check. Standalone `spec.size(expected)`, `spec.min(limit)`, and `spec.max(limit)` perform no type or finiteness check. `spec.size(NaN)` never matches because `NaN` is not equal to itself; `Infinity` can match `spec.size(Infinity)` or pass a minimum, and `-Infinity` can match `spec.size(-Infinity)` or pass a maximum.

---

## 5. Complex collections

### Objects and key validation

Object schemas validate the properties named in the schema. Input properties use normal JavaScript lookup, including inherited properties; an absent property is read as `undefined`. Validation is open by default: extra properties are preserved:

```javascript
const userSchema = spec({
  name: spec.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:

```javascript
const rangeSchema = spec({
  minVal: spec.number,
  maxVal: spec.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.

```javascript
// 1. Empty Array: matches any array
const anyArray = spec([]);
anyArray.assert([1, "two", {}]);

// 2. Repeating Array: matches all items
const numbersArray = spec([spec.number]);
numbersArray.assert([1, 2, 3]);
numbersArray.assert([]); // Also succeeds; use .min(1) to require an item
numbersArray.diff([1, "two"]); // Fails

// 3. Tuple: strictly closed positional array
const geoCoords = spec([spec.number, spec.number]);
geoCoords.assert([37.7749, -122.4194]);
geoCoords.diff([37.7749, -122.4194, "altitude"]);
// { kind: "size", expected: 2, input: [37.7749, -122.4194, "altitude"] }
```

The array shorthand uses `[]` for any array and `[schema]` for a repeating element schema. Add an exact size for empty and one-element arrays:

```javascript
const exactlyEmpty = spec([]).size(0);
exactlyEmpty.assert([]);
exactlyEmpty.diff(["extra"]); // Fails

const oneString = spec([spec.string]).size(1);
oneString.assert(["only"]);
oneString.diff([]);               // Fails: missing element
oneString.diff(["one", "two"]);   // Fails: extra element
oneString.diff([42]);             // Fails: incorrect element type
```

---

## 6. Handling absence and undefined values

The following helpers validate optional or missing data:

* **`spec.any()`**: Matches any value except `undefined` (equivalent to defined).
* **`spec.optional(schema)`**: Matches `undefined` or the nested schema.
* **`spec.nullable(schema)`**: Matches `null` or the nested schema.

These helpers can be nested:

```javascript
const maybeMiddleName = spec.optional(spec.nullable(spec.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: spec.sting }`):

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

Use `spec.undefined` to explicitly validate `undefined` values or missing properties:

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

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

---

## 7. Union types

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

```javascript
const stringOrNumberOrNull = spec.any(spec.string, spec.number, spec.null);

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

Branches are tried in order until one succeeds. Each failed branch's mismatch is retained during that pass. If a later branch succeeds, those mismatches are discarded; if every branch fails, they become the union's `expected` details. Each branch therefore runs at most once during a union check, and a failed union reports the exact results that caused it to fail. This also applies to `spec.optional()` and `spec.nullable()`, which use unions internally.

---

## 8. Error tagging and localization

The `.tag(label)` method returns a new spec with a label on its last constraint. It leaves the original spec unchanged. Every label except `undefined` is attached unchanged to failures as `mismatch.tag`; `undefined` means no tag.

### Single tag fallback

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

```javascript
const ageSchema = spec.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:

```javascript
const ageSchema = spec.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 its own label or the closest label to its right in the chain. Earlier labels take precedence over a later fallback; constraints added after the last tag remain untagged:

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

// Fails spec.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
```

### Composing and replacing tags

`.and(existingSpec)` preserves the appended spec's chain and labels. Calling `.tag()` on the result replaces the label on its last constraint; it does not replace earlier labels:

```javascript
const positive = spec.number.tag('number required').min(1).tag('positive required');
const combined = spec.any().and(positive).tag('combined minimum');

combined.diff('x').tag; // 'number required'
combined.diff(0).tag;   // 'combined minimum'
positive.diff(0).tag;   // 'positive required'; the original is unchanged
```

Likewise, `spec.string.tag('first').tag('second')` uses `'second'`: both calls label the same constraint.
Passing `undefined` instead removes the label from that last constraint; labels on earlier constraints in the chain are unaffected.

### Nested objects, arrays, and unions

An object or array spec returns its child's mismatch, then applies its own tag if that tag is defined. A container tag therefore overwrites a more specific child tag. Without a container tag, the child tag survives:

```javascript
const name = spec.string.tag('name must be a string');
const record = spec({ name });

record.diff({ name: 42 }).tag;               // 'name must be a string'
record.tag('invalid record').diff({ name: 42 }).tag; // 'invalid record'
spec([name]).tag('invalid list').diff([42]).tag;     // 'invalid list'
```

A union has its own mismatch, with branch failures inside `expected`. A tag on the union labels that outer mismatch and leaves the branch tags intact:

```javascript
const maybeName = spec.optional(name).tag('invalid optional name');
const nameMismatch = maybeName.diff(42);

nameMismatch.tag;             // 'invalid optional name'
nameMismatch.expected[1].tag; // 'name must be a string'
```

Without the union tag, the outer mismatch is untagged even when a branch has a tag. This applies to `spec.any(...)`, `spec.optional()`, and `spec.nullable()`.

### Opaque tag values

Tags can be strings, symbols, numbers, booleans, `null`, or other values. Values such as `0`, `false`, `''`, `NaN`, and `null` remain exact and form the same propagation boundary as any other tag. An explicit container tag also replaces a child tag regardless of its value. Use `undefined` to leave a constraint untagged and allow a later fallback through.

If a mismatch has no tag, `.assert()` uses `"Assertion failed"` unless an explicit reason was supplied. Otherwise it uses `"tag: "` followed by the tag converted to a string; the original value remains unchanged in `error.cause.tag`.

### Localization using symbols

Use `Symbol` values to resolve translation keys dynamically:

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

const ageSchema = spec.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 native `Error`. It exposes these properties:

- `message`: The custom reason passed to `.assert()` when it is not `null` or `undefined`; otherwise `"tag: "` followed by a failing tag converted to a string, or `"Assertion failed"` if there is no tag.
- `cause`: The underlying mismatch object returned by `.diff()`.
- `input`: The original input value that failed validation.
- `stack`: The runtime-formatted stack captured at the assertion, when the runtime provides one.

`cause` and `input` are enumerable, so `JSON.stringify(error)` includes both. Native Error properties such as `message` and `stack` remain non-enumerable; construct an explicit plain object when those fields are also needed in serialized output.

Because these enumerable fields can reach logs, do not pass secrets to `.assert()` unless that exposure is acceptable. The [security guide](security.md#sensitive-values-in-errors) explains the risk, and the [sensitive-data recipe](recipes.md#handling-sensitive-values) shows an alternative.

This catch is only a demonstration of the error's contents. In application code, handle a broken invariant at the request or process error boundary and stop the affected operation:

```javascript
try {
  remainingCapacity({ capacity: 10, used: 12 });
} catch (error) {
  console.log(error.message);    // "Invalid capacity state"
  console.log(error.cause.kind); // "predicate"
}
```

### Mismatch objects

`.diff()` returns a mismatch object on failure.

#### Common fields

Built-in constraints produce these fields:

- `kind`: A string matching the category of constraint failure (`'literal'`, `'type'`, `'predicate'`, `'size'`, `'min'`, `'max'`, `'any'`).
- `input`: The value that failed validation at this constraint. For nested schemas, this may be a child of the original value passed to `.diff()` or `.assert()`.
- `tag`: (Optional) The resolved tag label for the failing constraint.
- `path`: (Optional) Array of segments from the root to the failure. Strings are object property keys; numbers are array indexes. For example, `["users", 0, "name"]` identifies the `name` property of the first user. A numeric-looking object key stays a string: `["0", 0]` starts with a key named `"0"`, then an array index. Root-level failures have no `path`.

#### Constraint-specific fields

Other fields depend on the constraint `kind`:

| Failure `kind` | Description | `expected` value |
| :--- | :--- | :--- |
| `'literal'` | Value did not match literal. | The expected literal value. |
| `'type'` | Value was outside the expected value category. | The category description; `spec.number` reports `'finite number'`. |
| `'predicate'` | Custom predicate returned a falsy value. | String representation of the predicate function. |
| `'size'` | Exact value or size comparison failed. | The required value or size. |
| `'min'` | Inclusive lower-bound comparison failed. | The original lower limit. |
| `'max'` | Inclusive upper-bound comparison failed. | The original upper limit. |
| `'any'` | Value matched none of the schemas in the union. | Array of mismatch objects from each sub-schema. |

The failing value is in `input`. Mismatches do not include an `actual` field; callers can derive a value's type with JavaScript or the experimental `internals.kindOf(input)` helper when needed.

### Paths inside unions

`spec.optional()` and `spec.nullable()` use unions. A failed union reports its own path, with each branch's mismatch in `expected`. Nested branch paths are relative to that union's input:

```javascript
const configSchema = spec({
  database: spec.optional({ retries: spec.number.min(0) }),
});
const mismatch = configSchema.diff({ database: { retries: -5 } });

console.log(mismatch.kind);                    // "any"
console.log(mismatch.path);                    // ["database"]
console.log(mismatch.expected[1].kind);         // "min"
console.log(mismatch.expected[1].path);         // ["retries"]
```

---

## 10. Advanced validation examples

### Recursive validation

Use `spec.lazy(resolve)` when a schema refers to itself. It delays parsing until the first validation, after the variable holding the spec has been initialized:

```javascript
const treeSpec = spec.lazy(() => ({
  value: spec.string,
  children: [treeSpec],
}));

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

treeSpec.assert(validTree); // Succeeds
```

The argument to `spec.lazy()` must be a function and is checked immediately. The resolver may return anything accepted by `spec()`. It runs on the first validation, and the parsed spec is then cached. A resolver error or invalid returned schema propagates to the caller and is not cached. Validation delegates to the resolved spec, preserving the original mismatch details and complete path:

```javascript
const treeMismatch = treeSpec.diff({
  value: 'root',
  children: [{ value: 'branch', children: [{ value: 42, children: [] }] }],
});
treeMismatch.kind; // 'type'
treeMismatch.path; // ['children', 0, 'children', 0, 'value']
```

`spec.lazy()` enables recursive schemas; it does not detect cycles in the input. Validating cyclic or excessively deep data can exhaust the JavaScript stack.

### Class instance validation

Validate instance types using `instanceof` inside a custom predicate:

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

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

---

## 11. Where to go next

The sections above cover the complete public API and its mismatch output. The separate [recipes](recipes.md) collect copyable caller-side techniques. The [security guide](security.md) brings together the operational risks, limits, and remedies that also appear beside relevant examples in this walkthrough.

For the reasoning behind the API and its scope, see the [design decisions](design-decisions.md). Spekvet deliberately keeps application policy in ordinary JavaScript, where it can be explicit and adapted to the caller's needs.
