fast-check review
fast-check 4.9.0 generates inputs for JavaScript and TypeScript tests, checks a rule against each input, and reduces a failure to a smaller counterexample. The useful artifact is the seed and shrink path printed with that counterexample: both can reproduce the run. It also covers stateful models and scheduled promises, two jobs that fixture generators do not attempt. This release introduces chainUntil for shrinkable entity graphs, fixes shared state in entityGraph, and gives stringMatching alternatives equal selection weight. In our package checks, CommonJS require and ESM import both loaded successfully and TypeScript declarations were included.
fast-check 4.9.0 installed in 0.5 seconds and occupied 2 MB in our sandbox, with 0 audit findings, bundled types, and replayable shrinking for failed properties. Add it where the code has a genuine invariant; use ordinary example tests for exact business scenarios and UI states.
We installed it
| Install | ✓ · 0.5s | 2 packages on disk · 2 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 55.4 KB | gzipped (162.4 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does fast-check install cleanly?
Yes. In a fresh container with an empty cache, npm install fast-check finished in 0.5s, leaving 2 packages and 2 MB on disk. npm audit reported no known vulnerabilities.
How much does fast-check add to a browser bundle?
55.4 KB gzipped (162.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does fast-check work with both ESM and CommonJS?
Yes. Both import 'fast-check' and require('fast-check') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does fast-check include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
fast-check or @fast-check/vitest: which should you use?
@fast-check/vitest: Choose the official Vitest adapter when property cases should use test.prop and the runner's normal reporting. fast-check 4.9.0 installed in 0.5 seconds and occupied 2 MB in our sandbox, with 0 audit findings, bundled types, and replayable shrinking for failed properties.
When should you not use fast-check?
Most checks are fixed UI states or exact request payloads, where named examples explain the requirement more clearly than generated cases
Discussed on
Use it if
- A parser, serializer, sorter, validator, or numeric routine has a rule that should hold across a wide input range
- You want a failing random case reduced to a small input and replayable from the seed plus shrink path
- A cache, queue, reducer, or other stateful object can be compared with a simpler command model
- Promise ordering may hide a race and you can route the relevant asynchronous work through fc.scheduler
- Most checks are fixed UI states or exact request payloads, where named examples explain the requirement more clearly than generated cases
- Each candidate performs costly database or network work; the documented default of 100 runs can turn one test into 100 side effects
- You need believable names, addresses, or catalog fixtures; @faker-js/faker targets fixture realism while fast-check targets counterexamples and shrinking
- The project cannot absorb the version 4 migration: character helpers were removed, string generation moved under fc.string options, and record output may have a null prototype
- CI discards the reported seed and path; a failure that cannot be replayed leaves the team investigating whichever input appears next
Setup reality
We installed fast-check 4.9.0 without a cache on Node 22. npm finished in 0.5 seconds, placed 2 packages on disk, and used 2 MB in total. The package declares 1 direct dependency and 0 peers; its unpacked payload is 1,448 KB. npm audit returned 0 known vulnerabilities. It ships TypeScript declarations, uses an ESM package layout with an exports map, and loaded through both require() and ESM import in our sandbox.
There are no credentials or config files. Install it as a development dependency and call fc.assert from the test runner you already use. The official Vitest and Jest adapters are separate packages for teams that want test.prop. Version 4 requires Node 12.17 or newer and its README lists TypeScript 5.0 as the supported floor for typed use.
The runner tries 100 generated cases by default. A predicate that writes to a database can therefore perform 100 writes before shrinking starts. Await fc.assert when using asyncProperty, reset mutable fixtures for every candidate, and set numRuns from the actual cost of the property. Heavy use of filter or fc.pre can exhaust the allowed skips; generating a valid range directly is usually cheaper.
A failure report carries the generated value, seed, and shrink path. Store the seed and path with the bug, replay them in fc.assert, then add a fixed regression example after the repair. Our all-exports browser build measured 162.4 KB minified and 55.4 KB gzipped. That is a real frontend cost, so test-only use should stay out of production bundles.
Patterns
Check a serializer round trip check-round-trip
import fc from 'fast-check';
fc.assert(
fc.property(fc.jsonValue(), (value) => {
return JSON.parse(JSON.stringify(value)) !== undefined;
}),
);fc.assert throws when the predicate returns false or throws. The runner then reduces the generated JSON value before reporting it.
Await storage round trips test-async-property
await fc.assert(
fc.asyncProperty(fc.uuid(), fc.jsonValue(), async (id, value) => {
await store.set(id, value);
expect(await store.get(id)).toEqual(value);
}),
{ numRuns: 40 },
);The outer fc.assert returns a promise for asyncProperty. Without await, a test runner can finish the case before generation or shrinking completes.
Generate typed account records compose-record
const accountArb = fc.record(
{
id: fc.uuid(),
email: fc.emailAddress(),
seats: fc.integer({ min: 1, max: 500 }),
},
{ noNullPrototype: true },
);Version 4 record values may use a null prototype. noNullPrototype produces ordinary objects for code that calls inherited Object methods.
Select text units in version 4 generate-v4-text
const labelArb = fc.string({ minLength: 1, maxLength: 60 });
const graphemeArb = fc.string({ unit: 'grapheme', maxLength: 20 });
const hexArb = fc.string({
unit: fc.constantFrom('0', '1', 'a', 'b', 'c', 'd', 'e', 'f'),
});Version 4 removed fc.char and fc.stringOf. Pass a built-in unit or an arbitrary as the unit option to fc.string.
Replay one reported counterexample replay-failure
fc.assert(propertyUnderTest, {
seed: 1527422598337,
path: '3:0:1',
endOnFailure: true,
});seed and path must come from the failure report. Together they jump to the same shrunken case; endOnFailure prevents another shrink pass.
Put regressions before random cases run-known-examples
fc.assert(fc.property(fc.string(), normalizeTwice), {
examples: [[''], ['\u0000'], [' spaced ']],
numRuns: 200,
});Each item in examples is the argument array for one property call. Those cases run before the 200 generated candidates.
Generate an array with a valid index derive-dependent-input
const arrayAndIndexArb = fc
.array(fc.integer(), { minLength: 1 })
.chain((items) =>
fc.tuple(
fc.constant(items),
fc.integer({ min: 0, max: items.length - 1 }),
),
);chain lets the index bounds depend on the generated array and preserves shrinking. A broad index followed by filter would discard invalid candidates.
Exclude zero divisors apply-precondition
fc.assert(
fc.property(fc.integer(), fc.integer(), (left, right) => {
fc.pre(right !== 0);
return Math.abs(left % right) < Math.abs(right);
}),
);fc.pre discards a candidate rather than passing it. Too many discarded candidates make the property fail with an exhaustion error.
Run commands against a model test-command-model
fc.assert(
fc.property(commandArb, (commands) => {
fc.modelRun(
() => ({ model: [], real: new Stack() }),
commands,
);
}),
);Every generated command needs check and run methods. modelRun shrinks the command sequence that causes the real stack to diverge from the array model.
Explore promise completion orders schedule-promises
await fc.assert(
fc.asyncProperty(fc.scheduler(), async (scheduler) => {
const first = scheduler.schedule(loadUser('a'));
const second = scheduler.schedule(loadUser('b'));
await scheduler.waitIdle();
await Promise.all([first, second]);
}),
);fc.scheduler controls only promises scheduled through it. Calls that bypass scheduler.schedule retain their runtime completion order.
Set project-wide run limits configure-runner
fc.configureGlobal({
numRuns: process.env.CI ? 500 : 50,
interruptAfterTimeLimit: 30_000,
markInterruptAsFailure: true,
});configureGlobal affects later properties in the same process. Load it once from the test runner's setup file before test modules execute.
Sample a URL arbitrary inspect-generator
const urls = fc.sample(
fc.webUrl({ validSchemes: ['https'] }),
{ seed: 42, numRuns: 10 },
);
console.log(urls);fc.sample produces 10 inspectable values here. It does not run a property, report a failure, or shrink any sample.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @fast-check/vitest | npm | Choose the official Vitest adapter when property cases should use test.prop and the runner's normal reporting. |
| @faker-js/faker | npm | Choose it for plausible fixture records when shrinking and systematic edge cases are outside the job. |
| jsverify | npm | Keep it in an existing QuickCheck-style JavaScript suite when a migration would buy little. |
More testing guides
pytest · chai · vitest · jsdom · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

