Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.cbrock.dev/llms.txt

Use this file to discover all available pages before exploring further.

Every guard in ts-chas/guard exposes two async methods for producing valid test data: .generate() for quick sampling and .arbitrary() for deep integration with fast-check. Both are derived directly from the guard’s constraint chain, so the same guard that validates your data also drives its generation. No separate schemas or factory functions to maintain. fast-check is a peer dependency and must be installed separately:
npm install --save-dev fast-check
# or
pnpm add -D fast-check

.generate()

// Single value
const value = await guard.generate();

// Multiple values
const values = await guard.generate(n);
Returns a Promise<T> when called with no argument, or Promise<T[]> when called with a count. Generated values are guaranteed to pass the guard’s predicate.

Quick examples

// Primitives
await is.string.generate()                        // 'hello'
await is.number.int.gte(0).lte(100).generate()   // 42
await is.boolean.generate()                       // true
await is.bigint.positive.generate()               // 1000n

// Multiple values
await is.string.email.generate(5)
// ['a@b.com', 'x@y.net', ...]

// Objects
await is.object({ name: is.string.min(1), age: is.number.int.gte(0) }).generate()
// { name: 'abc', age: 7 }

// Arrays
await is.array(is.number.positive).nonEmpty.generate(3)
// [[1.2, 5], [0.3], [9, 0.1, 4]]

Data-driven tests with it.each

A common pattern is to generate a batch of valid inputs and feed them to parameterized tests:
const users = await is.object({
  name: is.string.min(1).max(50),
  email: is.string.email,
  age:   is.number.int.between(18, 120),
}).generate(20);

it.each(users)('processes user %o', user => {
  expect(processUser(user)).not.toThrow();
});

.arbitrary()

const arb = await guard.arbitrary();
Returns a Promise<Arbitrary<T>> — a fully configured fast-check Arbitrary that you can compose, transform, and use directly in fc.assert property tests.

Property-based testing

import * as fc from 'fast-check';

const emailGuard = is.string.email;
const arb = await emailGuard.arbitrary();

fc.assert(
  fc.property(arb, email => {
    const result = parseEmail(email);
    return result.domain.length > 0;
  })
);

Composing arbitraries

Because .arbitrary() returns a standard fast-check Arbitrary, you can use .map(), .filter(), and .chain() on it:
const idArb = await is.string.uuid().arbitrary();

// Map: derive a related shape from the valid input
const userArb = idArb.map(id => ({ id, createdAt: new Date() }));

// Filter: post-hoc narrowing (prefer guard constraints where possible)
const evenArb = (await is.number.int.arbitrary()).filter(n => n % 2 === 0);

// Chain: dependent generation
const arrayArb = await is.array(is.string.min(1)).nonEmpty.arbitrary();
const firstElementArb = arrayArb.chain(arr => fc.constant(arr[0]));

Combining multiple guards

const nameArb  = await is.string.min(1).max(50).arbitrary();
const emailArb = await is.string.email.arbitrary();
const ageArb   = await is.number.int.between(18, 120).arbitrary();

fc.assert(
  fc.property(nameArb, emailArb, ageArb, (name, email, age) => {
    const user = createUser({ name, email, age });
    return is.string.email(user.email);
  })
);

Constraint coverage

The generator reads constraints from the guard’s accumulated metadata and translates them to tight arbitraries. What follows is a full reference for every supported helper.

Strings

HelperGenerated values
.emailuser@domain.tld format
.urlWHATWG-valid URL strings
.uuid()UUID v4 by default
.hostnameRFC 1123 hostnames (foo.example.com)
.ipv4Dotted-quad IPv4 addresses
.ipv6Normalized IPv6 addresses
.cidrv4IPv4 CIDR blocks (10.0.0.0/8)
.cidrv6IPv6 CIDR blocks (normalized via URL parser)
.mac()MAC addresses; delimiter respected
.ulid26-character Crockford Base32 ULIDs
.cuidc + 24 alphanumeric characters
.cuid2Lowercase alphanumeric starting with a letter
.nanoid()NanoID using correct alphabet; length respected
.base64()Base64-encoded byte arrays
.hex()Hex strings; case, prefix, and evenLength respected
.hash()Correct byte-length output for each algorithm and encoding
.jwt()Structurally valid three-part JWT strings
.emojiStrings containing at least one emoji
.boolStrOne of: 'true', 'false', '1', '0', 'yes', 'no', 'on', 'off'
.json()Valid JSON strings
.isoFull ISO 8601 datetime strings
.iso.dateYYYY-MM-DD strings
.iso.datetime()Full datetime; offset, local, and precision respected
.min(n)Strings of at least n characters
.max(n)Strings of at most n characters
.length(n)Strings of exactly n characters
.regex(re)Strings matching the regex (via fc.stringMatching)
.includes(sub)Strings containing the substring
.startsWith(pfx)Strings with the given prefix
.endsWith(sfx)Strings with the given suffix

Numbers

HelperGenerated values
.intSafe integers
.int32Integers in [-2147483648, 2147483647]
.positiveNumbers strictly greater than 0
.nonnegativeNumbers >= 0
.negativeNumbers strictly less than 0
.nonpositiveNumbers <= 0
.gt(n) / .gte(n)Numbers above the bound
.lt(n) / .lte(n)Numbers below the bound
.between(min, max)Numbers within the range
.portIntegers in [0, 65535]
.unitFloats in [0, 1]
.multipleOf(n)Multiples of n
.evenEven integers (via multipleOf: 2)
.oddOdd integers
.digits(n)Integers with exactly n digits
.precision(n)Numbers with at most n decimal places

BigInts

BigInt has no native JSON Schema representation, so constraints are stored as internal markers.
HelperGenerated values
.positiveBigInts > 0n
.nonnegativeBigInts >= 0n
.negativeBigInts < 0n
.nonpositiveBigInts <= 0n
.gt(n) / .gte(n)BigInts above the bound
.lt(n) / .lte(n)BigInts below the bound
.between(min, max)BigInts within the range
.evenEven bigints
.oddOdd bigints
.multipleOf(n)Multiples of n
.digits(n)BigInts with exactly n digits
.int32BigInts in the 32-bit signed integer range
.int64BigInts in the 64-bit signed integer range

Booleans

HelperGenerated values
.trueAlways true
.falseAlways false
.asStringAlways 'true' or 'false'

Dates

HelperGenerated values
.after(d)Dates strictly after d
.before(d)Dates strictly before d
.between(min, max)Dates within the range
.futureDates after Date.now() at generation time
.pastDates before Date.now() at generation time
.year(n)Dates within the given calendar year
.weekendSaturdays and Sundays
.weekdayMonday through Friday
.day('monday')The specific day of the week
.month(n)The specific month (0-indexed)
.dayOfMonth(n)The specific day of the month
.hour(n)Dates with the specific hour
.minute(n)Dates with the specific minute
.second(n)Dates with the specific second
.millisecond(n)Dates with the specific millisecond
Dates are always generated within the year range 1000-9000 by default, so .toISOString() never throws.

URLs

is.url() returns a URL guard (not a string guard). Constraints from URL helpers are reflected in generation:
HelperGenerated values
(base is.url())Valid http:// or https:// URLs
.httphttp:// URLs with real domain hostnames
.https / .securehttps:// URLs with real domain hostnames
.localhttp://localhost:PORT URLs
.port(n)URLs with the specified port
.hasSearchURLs with a query string
.hasHashURLs with a hash fragment

Arrays

HelperGenerated values
.nonEmptyArrays with at least one element
.emptyZero-element arrays
.min(n)Arrays with at least n elements
.max(n)Arrays with at most n elements
.size(n)Arrays with exactly n elements
.uniqueArrays with no duplicate elements (uses fc.uniqueArray)
.includes(item)Arrays that always contain item as the first element

Tuples

Fixed-position tuples generate the correct type at each index:
const guard = is.tuple([is.string, is.number, is.boolean]);
await guard.generate()
// ['hello', 42, true]
Variadic tuples append rest elements after the fixed positions:
const guard = is.tuple([is.string], is.number);
await guard.generate()
// ['hello', 1, 5, 2]

Objects

Shaped objects generate a value for every key in the schema. Optional and nullable fields are handled automatically:
const guard = is.object({
  id:    is.string.uuid(),
  name:  is.string.min(1),
  score: is.number.nonnegative.optional,
  tag:   is.string.nullable,
});
await guard.generate()
// { id: 'a1b2...', name: 'x', score: undefined, tag: null }
HelperEffect on generation
.minSize(n)Object has at least n keys (shapeless objects only)
.maxSize(n)Object has at most n keys (shapeless objects only)
.size(n)Object has exactly n keys (shapeless objects only)

Records

is.record(keyGuard, valueGuard) generates plain objects whose keys satisfy keyGuard and whose values satisfy valueGuard:
const guard = is.record(is.string, is.number);
await guard.generate()
// { foo: 1.2, bar: -3 }
When the key guard has a finite value set (literals or enums), those exact keys are used.

Maps and Sets

await is.map(is.string, is.number).generate()
// Map { 'key' => 1 }

await is.set(is.number).minSize(2).generate()
// Set { 4, 9 }
Sets use fc.uniqueArray internally so size constraints are always satisfied even after deduplication.

Unions, intersections, and discriminated unions

// Union: values from either branch
await is.union(is.string, is.number).generate(10)
// mix of strings and numbers

// Discriminated union: correct discriminant on every value
const shape = is.discriminatedUnion('kind', {
  circle: is.object({ radius: is.number.positive }),
  square: is.object({ side: is.number.positive }),
});
await shape.generate()
// { kind: 'circle', radius: 3.5 }
Intersections generate from the first branch (exact intersection generation is not tractable in the general case).

Fallback behavior

Guards that cannot be mapped to a specific arbitrary fall back to fc.anything(). This covers custom guards created with .where(), .and(), lazy guards, and any other guard whose structure is opaque to the generator. These will produce values that may or may not pass the guard; use .arbitrary() and add a .filter(guard) step if you need guaranteed correctness for custom predicates:
const myGuard = is.number.where(n => someComplexCheck(n));
const arb = (await myGuard.arbitrary()).filter(myGuard);

Combining with property-based testing

.arbitrary() slots directly into fc.property for full property-based test suites:
import * as fc from 'fast-check';
import { is } from 'ts-chas/guard';

const invoiceArb = await is.object({
  id:       is.string.uuid(),
  amount:   is.number.positive,
  currency: is.literal('USD', 'EUR', 'GBP'),
  lines:    is.array(is.object({
    description: is.string.min(1),
    quantity:    is.number.int.gte(1),
    unitPrice:   is.number.positive,
  })).nonEmpty,
}).arbitrary();

fc.assert(
  fc.property(invoiceArb, invoice => {
    const total = computeTotal(invoice);
    return total > 0 && is.number.positive(total);
  }),
  { numRuns: 200 }
);

Implementation notes

  • fast-check is loaded lazily on the first call to .generate() or .arbitrary(). If it is not installed, a clear error is thrown with installation instructions.
  • The generator reads from guard.meta.jsonSchema, the same accumulated metadata object that drives .toJsonSchema(). Adding a constraint to a guard automatically affects both JSON Schema output and data generation.
  • Constraint chaining composes correctly: is.number.int.positive.lte(100) generates integers in [1, 100] because each helper merges its contribution into the shared metadata object.
  • Dates are clamped to the year range 1000-9000 by default to prevent toISOString() from throwing on extreme values. Explicit .after() or .before() bounds override the defaults.