| name | nw-pbt-typescript |
| agent | nw-functional-software-crafter |
| description | TypeScript/JavaScript property-based testing with fast-check framework and arbitraries |
| user-invocable | false |
PBT TypeScript -- fast-check
Framework Selection
fast-check is the dominant PBT framework for TypeScript/JavaScript. No serious competitors.
- 8+ years mature, very actively maintained
- First-class TypeScript types
- Zero runtime dependencies
- Used by jest, jasmine, fp-ts, ramda, js-yaml
Quick Start
import fc from 'fast-check';
test('sort is idempotent', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (arr) => {
const sorted = [...arr].sort((a, b) => a - b);
const twice = [...sorted].sort((a, b) => a - b);
expect(sorted).toEqual(twice);
}),
{ numRuns: 1000 }
);
});
Generator (Arbitrary) Cheat Sheet
Primitives
fc.integer()
fc.integer({ min: 0, max: 99 })
fc.nat()
fc.float()
fc.double()
fc.string()
fc.string({ minLength: 1, maxLength: 50 })
fc.boolean()
fc.constant(null)
fc.constantFrom(1, 2, 3)
Collections
fc.array(fc.integer())
fc.array(fc.integer(), { minLength: 1, maxLength: 10 })
fc.set(fc.integer())
fc.dictionary(fc.string(), fc.integer())
fc.tuple(fc.integer(), fc.string())
Combinators
fc.oneof(fc.integer(), fc.string())
fc.option(fc.integer())
fc.integer().map(n => n * 2)
fc.integer().filter(n => n > 0)
fc.array(fc.integer(), { minLength: 1 }).chain(
arr => fc.tuple(fc.constant(arr), fc.integer({ min: 0, max: arr.length - 1 }))
)
fc.record({
name: fc.string({ minLength: 1 }),
age: fc.integer({ min: 0, max: 150 }),
active: fc.boolean(),
})
fc.(
{ : , : fc.() },
{ : , : fc.() },
{ : , : fc.(, ) }
)
Recursive
const jsonArb = fc.letrec(tie => ({
value: fc.oneof(
fc.constant(null), fc.boolean(), fc.integer(), fc.string(),
fc.array(tie('value')), fc.dictionary(fc.string(), tie('value'))
),
})).value;
Stateful Testing (Model-Based)
import fc from 'fast-check';
type Model = { items: Map<string, number> };
class PutCommand implements fc.Command<Model, MyStore> {
constructor(readonly key: string, readonly value: number) {}
check = (m: Readonly<Model>) => true;
run(m: Model, r: MyStore): void {
r.put(this.key, this.value);
m.items.set(this.key, this.value);
}
toString = () => `put(${this.key}, ${this.value})`;
}
class GetCommand implements fc.<, > {
() {}
(: <>): {
m..(.);
}
(: , : ): {
(r.(.)).(m..(.));
}
toString = ;
}
allCommands = [
fc.(fc.(), fc.()).( (k, v)),
fc.().( (k)),
];
(, {
fc.(
fc.(fc.(allCommands), {
= () => ({
: { : () },
: (),
});
fc.(setup, cmds);
})
);
});
Race Condition Testing
fc.assert(
fc.property(fc.scheduler(), fc.commands(allCommands), async (s, cmds) => {
const setup = () => ({
model: { items: new Map() },
real: new MyStore(s),
});
await fc.scheduledModelRun(setup, cmds);
})
);
Scheduler controls promise resolution order, enabling deterministic exploration of async interleavings.
Test Runner Integration
import { test } from '@fast-check/jest';
test.prop([fc.integer(), fc.integer()])('commutative addition', (a, b) => {
expect(a + b).toBe(b + a);
});
import { test } from '@fast-check/vitest';
test.prop([fc.string()])('string length non-negative', (s) => {
expect(s.length).toBeGreaterThanOrEqual(0);
});
fc.assert(
fc.property(fc.integer(), (n) => { }),
{ seed: 1234567890, path: '4:1:0' }
);
Unique Features
- Race condition detection:
fc.scheduler() controls async interleaving -- unique outside Erlang
- Replay: Seed + path for deterministic reproduction
- Bias mode: Automatically tests edge cases (0, -1, MAX_INT, empty) more often
- Verbose mode: Shows all generated values and shrink steps
- Integrated shrinking: Automatic via shrink trees, composes with generators
- Size parameter:
{ size: '+1' } controls generation complexity growth