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

ts-fuzzing

v0.1.2

Published

TypeScript-driven value fuzzing and quick-checking with optional UI adapters

Downloads

38

Readme

ts-fuzzing

Generate values from TypeScript types or schemas, then run quick-checks or fuzzing against arbitrary callbacks and UI components.

API quick reference

| Workflow | Helpers | | --- | --- | | Generate values | sampleValues, sampleBoundaryValues, sampleProps, sampleBoundaryProps, sampleValuesFromSchema, sampleBoundaryValuesFromSchema | | Property-based fuzzing | fuzzValues, quickCheckValues, fuzzValuesGuided, fuzzValuesMulti, replayValues, replayFromError | | Component fuzzing | fuzzComponent, quickCheckComponent, fuzzComponentGuided, fuzzReactComponent (ts-fuzzing/react), createVueDomRender (ts-fuzzing/vue), createSvelteRender (ts-fuzzing/svelte) | | Invariant helpers | fuzzRoundtrip, fuzzIdempotent, fuzzCommutative, fuzzAssociative, fuzzMonotonic | | Stateful / sequence | fuzzStateful, StatefulFuzzError | | Differential | fuzzDifferential | | Corpus / regression | loadCorpus, saveCorpus, appendToCorpus, fuzzFromCorpus, fuzzFromCorpusWithMutation | | Mutation | mutateValue, generateMutations | | Statistics | collectStatistics, formatStatistics, formatGuidedReport | | Failure tooling | ValueFuzzError, ComponentFuzzError, renderReproTest, writeReproTest | | Low-level | analyzeTypeDescriptor, analyzePropsDescriptor, arbitraryFromDescriptor, boundaryValuesFromDescriptor, schemaSupportFromSchema | | Marker types | UUID, ULID, ISODateString, Int, Float, Min, Max, MinLength, MaxLength, MinItems, MaxItems, Pattern<...> |

Install

Requirements:

  • Node.js 24+
  • ESM ("type": "module")
pnpm add -D ts-fuzzing vitest

# Optional schema support
pnpm add -D zod valibot

# Optional React adapter
pnpm add react react-dom

# Optional framework adapters
pnpm add vue
pnpm add svelte

Quick Start

Use exported TypeScript types as the source of truth, then run a generic consumer callback with generated values. The examples below assume you are writing vitest test cases.

import { expect, test } from "vitest";
import { fuzzValues } from "ts-fuzzing";

type SearchQuery = {
  page?: number;
  term: string;
};

test("fuzzes a callback with values generated from a TypeScript type", async () => {
  await expect(
    fuzzValues<SearchQuery>({
      sourcePath: new URL("./search.ts", import.meta.url),
      typeName: "SearchQuery",
      numRuns: 200,
      seed: 42,
      run(value) {
        executeSearch(value);
      },
    }),
  ).resolves.toBeUndefined();
});

Failures throw ValueFuzzError.

import { expect, test } from "vitest";
import { ValueFuzzError, fuzzValues } from "ts-fuzzing";

test("inspects the minimized failing value", async () => {
  expect.assertions(4);

  try {
    await fuzzValues<SearchQuery>({
      sourcePath: new URL("./search.ts", import.meta.url),
      typeName: "SearchQuery",
      seed: 42,
      run(value) {
        executeSearch(value);
      },
    });
  } catch (error) {
    expect(error).toBeInstanceOf(ValueFuzzError);
    if (error instanceof ValueFuzzError) {
      expect(error.failingValue).toBeDefined();
      expect(error.seed).toBe(42);
      expect(error.warnings).toBeInstanceOf(Array);
    }
  }
});

Common Usage

Sample generated values

sampleValues* returns an async iterator. Consume it with for await...of when you want a snapshot in a test. Reuse the same seed to replay the same yielded sequence.

import { expect, test } from "vitest";
import { sampleValues } from "ts-fuzzing";

type SearchQuery = {
  page?: number;
  term: string;
};

test("samples generated values for snapshot-like inspection", async () => {
  const values: SearchQuery[] = [];
  for await (const value of sampleValues<SearchQuery>({
    sourcePath: new URL("./search.ts", import.meta.url),
    typeName: "SearchQuery",
    numRuns: 8,
    seed: 1,
  })) {
    values.push(value);
  }

  expect(values).toHaveLength(8);
  expect(values[0]).toHaveProperty("term");
});

Boundary-focused checks

import { expect, test } from "vitest";
import { quickCheckValues } from "ts-fuzzing";

test("checks boundary-focused cases", async () => {
  const report = await quickCheckValues<SearchQuery>({
    sourcePath: new URL("./search.ts", import.meta.url),
    typeName: "SearchQuery",
    maxCases: 64,
    run(value) {
      executeSearch(value);
    },
  });

  expect(report.checkedCases).toBeGreaterThan(0);
  expect(report.totalCases).toBeGreaterThan(0);
});

Type-level fuzzing hints

Keep the original base type visible and add fuzzing-specific hints through intersections.

import type {
  ISODateString,
  Int,
  Max,
  MaxLength,
  Min,
  MinLength,
  Pattern,
  UUID,
} from "ts-fuzzing";

export type SearchQuery = {
  id: string & UUID;
  createdAt: string & ISODateString;
  page: number & Int & Min<1> & Max<10>;
  term: string & MinLength<1> & MaxLength<32>;
  contact: string & Pattern<"email">;
};

These hints affect fuzzing only. They do not validate runtime values by themselves.

Schema-driven values

import { expect, test } from "vitest";
import * as z from "zod";
import { fuzzValues, sampleValuesFromSchema } from "ts-fuzzing";

const querySchema = z.object({
  term: z.string().min(1).max(32),
  page: z.number().int().min(1).max(10),
});

test("generates normalized values directly from schema", async () => {
  const values: Array<z.infer<typeof querySchema>> = [];
  for await (const value of sampleValuesFromSchema({
    schema: querySchema,
    numRuns: 16,
    seed: 1,
  })) {
    values.push(value);
  }

  expect(values).toHaveLength(16);
});

test("fuzzes a callback from schema output", async () => {
  await expect(
    fuzzValues({
      schema: querySchema,
      numRuns: 100,
      seed: 1,
      run(value) {
        executeSearch(value);
      },
    }),
  ).resolves.toBeUndefined();
});

React components

Use the React adapter when the consumer is a component rather than a plain callback.

import { expect, test } from "vitest";
import { fuzzReactComponent } from "ts-fuzzing/react";
import { Button } from "./Button.js";

test("fuzzes a React component from its exported props type", async () => {
  await expect(
    fuzzReactComponent({
      component: Button,
      sourcePath: new URL("./Button.tsx", import.meta.url),
      exportName: "Button",
      numRuns: 200,
      seed: 42,
    }),
  ).resolves.toBeUndefined();
});

By default React uses renderToStaticMarkup. Use createReactDomRender() if you need mount-time failures such as useEffect crashes.

import { expect, test } from "vitest";
import { createReactDomRender, fuzzReactComponent } from "ts-fuzzing/react";

test("catches mount-time React failures with the DOM renderer", async () => {
  await expect(
    fuzzReactComponent({
      component: EffectBomb,
      sourcePath: new URL("./EffectBomb.tsx", import.meta.url),
      exportName: "EffectBomb",
      render: createReactDomRender(),
      numRuns: 100,
      seed: 1,
    }),
  ).rejects.toThrow();
});

Provider fuzzing

import { expect, test } from "vitest";
import { createReactDomRender, quickCheckReactComponent } from "ts-fuzzing/react";

test("quick-checks provider and component inputs together", async () => {
  const report = await quickCheckReactComponent({
    component: ThemePanel,
    sourcePath: new URL("./ThemePanel.tsx", import.meta.url),
    exportName: "ThemePanel",
    render: createReactDomRender({
      providers: [
        {
          key: "themeProvider",
          component: ThemeProvider,
          sourcePath: new URL("./ThemeProvider.tsx", import.meta.url),
          exportName: "ThemeProvider",
          fixedProps: { locale: "en-US" },
        },
      ],
    }),
  });

  expect(report.checkedCases).toBeGreaterThan(0);
});

Vue and Svelte

Use fuzzComponent() with a framework-specific renderer.

import { expect, test } from "vitest";
import Widget from "./Widget.vue";
import { fuzzComponent } from "ts-fuzzing";
import { createVueDomRender } from "ts-fuzzing/vue";

test("fuzzes a Vue component through the generic component API", async () => {
  await expect(
    fuzzComponent({
      component: Widget,
      sourcePath: new URL("./Widget.vue", import.meta.url),
      render: createVueDomRender(),
      numRuns: 100,
      seed: 1,
    }),
  ).resolves.toBeUndefined();
});
import { expect, test } from "vitest";
import Widget from "./Widget.svelte";
import { quickCheckComponent } from "ts-fuzzing";
import { createSvelteRender } from "ts-fuzzing/svelte";

test("quick-checks a Svelte component through the generic component API", async () => {
  const report = await quickCheckComponent({
    component: Widget,
    sourcePath: new URL("./Widget.svelte", import.meta.url),
    render: createSvelteRender(),
    maxCases: 32,
  });

  expect(report.checkedCases).toBeGreaterThan(0);
});

Invariant helpers

fuzzRoundtrip, fuzzIdempotent, fuzzCommutative, fuzzAssociative, and fuzzMonotonic each skip the fast-check ceremony for a common property shape. The commutative/associative/monotonic variants internally draw two or three independent samples from the same descriptor or schema, so you still supply only the single-value source.

import { expect, test } from "vitest";
import * as z from "zod";
import { fuzzIdempotent, fuzzRoundtrip } from "ts-fuzzing";

const record = z.object({ name: z.string().min(1).max(16) });

test("JSON encode/decode roundtrips", async () => {
  await expect(
    fuzzRoundtrip({
      schema: record,
      numRuns: 100,
      encode(value) { return JSON.stringify(value); },
      decode(text) { return JSON.parse(text); },
    }),
  ).resolves.toBeUndefined();
});

test("trim is idempotent", async () => {
  await expect(
    fuzzIdempotent({
      schema: z.object({ text: z.string().max(8) }),
      apply(value) { return { text: value.text.trim() }; },
    }),
  ).resolves.toBeUndefined();
});

test("addition is commutative and associative", async () => {
  const pair = z.object({ n: z.number().int().min(-8).max(8) });
  await fuzzCommutative({ schema: pair, operation: (a, b) => a.n + b.n });
  await fuzzAssociative({ schema: pair, operation: (a, b) => ({ n: a.n + b.n }) });
});

test("doubling is monotonic", async () => {
  await fuzzMonotonic({
    schema: z.object({ n: z.number().int().min(-8).max(8) }),
    compareInput: (a, b) => a.n - b.n,
    mapping: ({ n }) => n * 2,
  });
});

Differential testing

fuzzDifferential runs two implementations against the same generated input and reports the first divergence.

import { fuzzDifferential } from "ts-fuzzing";

await fuzzDifferential({
  schema: inputSchema,
  implementations: [legacyImpl, rewriteImpl],
  names: ["legacy", "rewrite"],
  numRuns: 200,
});

Stateful / command sequence fuzzing

fuzzStateful generates a random sequence of actions, runs each against a reference model and the real implementation, and checks an invariant after every step.

import fc from "fast-check";
import { fuzzStateful } from "ts-fuzzing";

await fuzzStateful<{ items: number[] }, RealStack>({
  setup: () => ({ model: { items: [] }, real: new RealStack() }),
  actions: [
    {
      name: "push",
      generate: fc.integer(),
      apply({ model, real, input }) {
        model.items.push(input);
        real.push(input);
      },
    },
    {
      name: "pop",
      precondition: (model) => model.items.length > 0,
      apply({ model, real }) {
        model.items.pop();
        real.pop();
      },
    },
  ],
  invariant({ model, real }) {
    if (model.items.length !== real.size()) {
      throw new Error("length diverged");
    }
  },
  maxActions: 40,
  numRuns: 100,
});

Failures surface as StatefulFuzzError with a failingTrace describing the applied actions.

Seed replay

replayValues walks every iteration of a past fuzz run for a given seed, records whether each input passed or failed, and returns the full trace without stopping on the first failure. replayFromError takes a caught ValueFuzzError and rebuilds the run using its recorded seed.

import { ValueFuzzError, fuzzValues, replayFromError } from "ts-fuzzing";

try {
  await fuzzValues({ sourcePath, typeName: "SearchQuery", run: executeSearch });
} catch (error) {
  if (error instanceof ValueFuzzError) {
    const report = await replayFromError({
      error,
      sourcePath,
      typeName: "SearchQuery",
      run: executeSearch,
    });
    for (const step of report.failures) {
      console.log(step.iteration, step.value, step.cause);
    }
  }
  throw error;
}

onIteration lets you stream each step (e.g. to log every input that was attempted) without collecting the full trace in memory. Pass stopOnFirstFailure: true when you only want the first divergence but still need the preceding iterations.

Multi-failure collection

fuzzValues stops at the first failure so it can minimize the counterexample. When you want to find every distinct failing value in one pass (for example in a bug-sweep session), use fuzzValuesMulti. It generates samples with the same descriptor / schema pipeline, runs each input independently, deduplicates failures by serialized value, and returns a report.

import { fuzzValuesMulti } from "ts-fuzzing";

const report = await fuzzValuesMulti({
  schema,
  maxFailures: 5,
  numRuns: 500,
  run: executeSearch,
});

for (const failure of report.failures) {
  console.error(failure.iteration, failure.value, failure.cause);
}

report.failures lists up to maxFailures distinct failing inputs. Shrinking is intentionally skipped — run a single value through fuzzValues (or writeReproTest) when you want a minimized counterexample.

Statistics / coverage reporter

collectStatistics samples inputs through the same descriptor / schema pipeline and bucketizes each one through a user-supplied classify. It returns a sorted StatisticsReport you can assert on, and formatStatistics renders it as a human-readable histogram.

import { collectStatistics, formatStatistics } from "ts-fuzzing";

const report = await collectStatistics({
  schema: querySchema,
  numRuns: 500,
  classify(value) {
    if (value.page === 1) return "first-page";
    if (value.page === 5) return "last-page";
    return "middle";
  },
});

console.log(formatStatistics(report));
// 60.5%  [302/500]  middle
// 19.8%  [ 99/500]  first-page
// 19.8%  [ 99/500]  last-page

Return an array from classify to attach multiple labels per input (e.g. ["even", "boundary"]) and undefined to skip the input from the histogram.

formatGuidedReport(report) produces the equivalent text summary for the GuidedCoverageReport returned by fuzzValuesGuided / fuzzComponentGuided: total iterations, corpus size, every discovery with its iteration, reason, and the input that surfaced it.

Mutation

mutateValue and generateMutations apply constraint-aware tweaks (increment numbers by ±1/±2, edit a string character, push/pop an array entry, swap a nested object property) while keeping the value within the source or schema descriptor. Literals and enum branches are deliberately preserved so the value still matches its declared type. fuzzFromCorpusWithMutation expands every saved corpus entry into mutationsPerEntry neighbours and runs each through a check:

import {
  fuzzFromCorpusWithMutation,
  generateMutations,
  mutateValue,
} from "ts-fuzzing";

const nearby = generateMutations({
  schema,
  count: 10,
  seed: 1,
  value: savedFailure,
});

const report = await fuzzFromCorpusWithMutation({
  corpusPath,
  schema,
  mutationsPerEntry: 16,
  collectAllFailures: true,
  run: executeSearch,
});

The report has the same shape as fuzzFromCorpus plus attempts (total mutations tried) and, on each failure, both the origin corpus entry and the mutation that actually broke the check.

Regression corpus

appendToCorpus() stores a failing value from ValueFuzzError.failingValue, and fuzzFromCorpus() re-runs every stored entry on the next test. Map and Set values are preserved across save/load.

import {
  ValueFuzzError,
  appendToCorpus,
  fuzzFromCorpus,
  fuzzValues,
} from "ts-fuzzing";

const corpusPath = new URL("./search-regression.json", import.meta.url);

try {
  await fuzzValues({ schema, run: executeSearch });
} catch (error) {
  if (error instanceof ValueFuzzError) {
    appendToCorpus({ corpusPath, value: error.failingValue });
  }
  throw error;
}

// later, in the regression suite:
const report = await fuzzFromCorpus({
  corpusPath,
  collectAllFailures: true,
  run: executeSearch,
});
expect(report.failures).toEqual([]);

Minimal repro export

When a run fails, renderReproTest / writeReproTest turn the caught ValueFuzzError into a standalone test file so you can commit the failing input as a regression.

import { ValueFuzzError, fuzzValues, writeReproTest } from "ts-fuzzing";

try {
  await fuzzValues({ schema, run: executeSearch });
} catch (error) {
  if (error instanceof ValueFuzzError) {
    writeReproTest({
      error,
      outputPath: new URL("./search.repro.test.ts", import.meta.url),
      runnerImport: "./search.js",
      runnerSymbol: "executeSearch",
    });
  }
  throw error;
}

Progress hooks

Every long-running runner (fuzzValues, quickCheckValues, fuzzValuesMulti, fuzzFromCorpus, fuzzFromCorpusWithMutation, plus their component-level wrappers) accepts an onProgress callback throttled by progressIntervalMs (default 1000). The hook receives { iteration, elapsedMs, failures, totalRuns? } so you can stream a status line during a long fuzz run without polluting fast paths with per-iteration logging.

await fuzzValues({
  schema,
  numRuns: 10_000,
  progressIntervalMs: 2_000,
  onProgress({ iteration, totalRuns, failures, elapsedMs }) {
    console.log(`[${elapsedMs}ms] ${iteration}/${totalRuns} (${failures} failures)`);
  },
  run: executeSearch,
});

The hook always fires once on completion (or before throwing) so the final iteration / failure count is reported even when the configured interval would otherwise suppress it. Use progressIntervalMs: 0 to receive an event after every iteration.

Time budget

All value- and component-based fuzzers accept timeoutMs (wall-clock interrupt, not marked as a failure) and perRunTimeoutMs (per-iteration hard timeout) on top of numRuns.

await fuzzValues({
  schema,
  numRuns: 10_000,
  timeoutMs: 30_000,      // stop after 30 seconds even if runs remain
  perRunTimeoutMs: 500,   // kill a single iteration that hangs past 500 ms
  run: executeSearch,
});

Notes

  • sampleProps* remains available as a component-focused alias. React adapter exports live under ts-fuzzing/react, and Vue/Svelte render helpers live under ts-fuzzing/vue and ts-fuzzing/svelte.
  • source-based APIs cannot infer compile-time TypeScript types from sourcePath alone. Use explicit generics such as sampleValues<SearchQuery>(...) or fuzzValues<SearchQuery>(...) when you want editor typing.
  • zod and valibot can describe values directly. Generic Standard Schema support is treated as a validator overlay unless the vendor adapter exposes a descriptor.
  • unresolved generic parameters use their extends constraint when one can be generalized. If a generic cannot be generalized, ts-fuzzing emits a runtime warning and falls back to unknown value generation.
  • conditional types are supported when TypeScript already resolves them to concrete types or unions, and for simple unresolved conditionals that can be generalized from an extends constraint. Conditional types that rely on infer still fall back to unknown with a warning.
  • marker types such as UUID, ULID, ISODateString, Int, Float, Min, Max, MinLength, MaxLength, MinItems, MaxItems, and Pattern<...> are intended to be intersected with an explicit base type such as string & UUID or number & Int.
  • common external runtime types such as ReactNode, URL, Map, and Set are normalized into dedicated fuzzing descriptors.

Examples and Docs