
Security News
The Hidden Blast Radius of the Axios Compromise
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.
@justanotherdot/hedgehog
Advanced tools
Property-based testing library for TypeScript, inspired by Hedgehog
A property-based testing library for TypeScript, inspired by Jacob Stanley's Hedgehog library for Haskell. Hedgehog automatically generates test cases and provides integrated shrinking to find minimal failing examples.
This library features a unique AdaptiveSeed implementation that is used by default and transparently chooses the optimal random number generation strategy:
The default Seed export is AdaptiveSeed - it automatically selects the best approach based on operation patterns, providing optimal performance without user intervention.
npm install @justanotherdot/hedgehog
For Zod integration (optional):
npm install @justanotherdot/hedgehog zod
import { forAll, Gen, Config } from '@justanotherdot/hedgehog';
// Define a property: reversing a list twice gives the original list
const reverseTwiceProperty = forAll(
Gen.array(Gen.number()),
(list) => {
const original = [...list]; // Don't mutate original
const reversed = list.reverse().reverse();
return JSON.stringify(reversed) === JSON.stringify(original);
}
);
// Test with complex data structures
const userProperty = forAll(
Gen.object({
id: Gen.number({ min: 1, max: 1000 }),
name: Gen.optional(Gen.string()), // string | undefined
email: Gen.nullable(Gen.string()), // string | null
status: Gen.union( // 'active' | 'inactive' | 'pending'
Gen.literal('active'),
Gen.literal('inactive'),
Gen.literal('pending')
)
}),
(user) => {
// Property: user objects have valid structure
return typeof user.id === 'number' &&
user.id > 0 &&
['active', 'inactive', 'pending'].includes(user.status);
}
);
// Test the properties
const config = new Config(100); // Run 100 tests each
console.log('Reverse property:', reverseTwiceProperty.run(config).type === 'pass' ? 'PASSED' : 'FAILED');
console.log('User property:', userProperty.run(config).type === 'pass' ? 'PASSED' : 'FAILED');
See the examples/ directory for more comprehensive examples including advanced configuration and real-world testing scenarios.
Properties are statements that should be true for all valid inputs:
import { forAll, Gen, int, string } from 'hedgehog';
// Property: string length is preserved under concatenation
const concatenationProperty = forAll(
Gen.tuple(string(), string()),
([a, b]) => {
const result = a + b;
return result.length === a.length + b.length;
}
);
// Property: addition is commutative
const commutativeProperty = forAll(
Gen.tuple(int(0, 1000), int(0, 1000)),
([a, b]) => a + b === b + a
);
Generators produce random test data of specific types:
import { Gen } from 'hedgehog';
// Basic generators
const boolGen = Gen.bool();
const numberGen = Gen.int(1, 100);
const stringGen = Gen.string();
// Composite generators
const arrayGen = Gen.array(numberGen);
const objectGen = Gen.object({
id: numberGen,
name: stringGen,
active: boolGen
});
// Transformed generators
const evenNumberGen = numberGen
.filter(n => n % 2 === 0)
.map(n => n * 2);
Handle nullable, optional, and union types elegantly:
// Optional and nullable generators
const optionalName = Gen.optional(stringGen); // string | undefined
const nullableId = Gen.nullable(numberGen); // number | null
// Union types
const statusGen = Gen.union(
Gen.constant('pending'),
Gen.constant('success'),
Gen.constant('error')
); // 'pending' | 'success' | 'error'
// Discriminated unions for complex types
interface SuccessResult {
type: 'success';
data: string;
}
interface ErrorResult {
type: 'error';
message: string;
}
const resultGen = Gen.discriminatedUnion('type', {
success: Gen.object({
type: Gen.constant('success' as const),
data: stringGen
}),
error: Gen.object({
type: Gen.constant('error' as const),
message: stringGen
})
}); // SuccessResult | ErrorResult
// Weighted unions for probability control
const biasedBoolGen = Gen.weightedUnion([
[9, Gen.constant(true)], // 90% true
[1, Gen.constant(false)] // 10% false
]);
The Seed class provides deterministic random generation. By default, this is the AdaptiveSeed implementation which automatically optimizes performance:
import { Seed, Gen, Size } from 'hedgehog';
// This is AdaptiveSeed - automatically optimized
const seed = Seed.fromNumber(42);
const size = Size.of(10);
const gen = Gen.int(1, 100);
// Generate the same value every time with the same seed
const tree1 = gen.generate(size, seed);
const tree2 = gen.generate(size, seed);
console.log(tree1.value === tree2.value); // true
// Split seeds for independent generation
const [leftSeed, rightSeed] = seed.split();
const leftValue = gen.generate(size, leftSeed).value;
const rightValue = gen.generate(size, rightSeed).value;
// leftValue and rightValue are independent
// Check what implementation is being used
console.log(seed.getImplementation()); // 'wasm' | 'bigint' | 'bigint-fallback'
Generate test data directly from your Zod schemas:
import { z } from 'zod';
import { forAll, Config } from '@justanotherdot/hedgehog';
import { fromSchema } from '@justanotherdot/hedgehog/zod';
// Define your schema
const userSchema = z.object({
name: z.string(),
age: z.number().min(0).max(120),
email: z.string().email(),
active: z.boolean(),
tags: z.array(z.string()).max(5)
});
// Generate test data from schema
const userGenerator = fromSchema(userSchema);
// Property: generated data always validates
const validationProperty = forAll(userGenerator, (user) => {
return userSchema.safeParse(user).success;
});
// Run the test
const result = validationProperty.run(new Config(100));
console.log('Schema validation:', result.type === 'pass' ? 'PASSED' : 'FAILED');
// Sample generated data
console.log('Sample user:', userGenerator.sample());
// Output: { name: "hello", age: 42, email: "test@example.com", active: true, tags: ["tag1"] }
Works with nested objects, unions, arrays, and all Zod features:
const eventSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('login'),
userId: z.string().uuid(),
timestamp: z.date()
}),
z.object({
type: z.literal('logout'),
duration: z.number().positive()
})
]);
const eventGen = fromSchema(eventSchema);
const event = eventGen.sample();
// Generates either a login or logout event with proper types
The default Seed automatically chooses the optimal implementation:
import { Seed } from 'hedgehog';
const seed = Seed.fromNumber(42);
// Single operations: automatically uses WASM for speed
const [bool, newSeed] = seed.nextBool(); // 1.92x faster with WASM
// Bulk operations: automatically batches with WASM
const result = seed.nextBools(1000); // 18.37x faster with batching
// Complex workflows: automatically uses BigInt for efficiency
// (Multiple chained operations favor BigInt due to lower overhead)
Check which implementation is being used:
console.log(seed.getImplementation()); // 'wasm', 'bigint', or 'bigint-fallback'
const perfInfo = seed.getPerformanceInfo();
console.log(perfInfo.batchingAvailable); // true if WASM batching available
console.log(perfInfo.recommendedForBulkOps); // true if optimal for bulk operations
For advanced use cases, you can choose specific implementations:
import { Seed as BigIntSeed } from 'hedgehog/seed/bigint';
import { Seed as WasmSeed } from 'hedgehog/seed/wasm';
import { AdaptiveSeed } from 'hedgehog/seed/adaptive';
// Explicit BigInt usage (pure JavaScript, works everywhere)
const bigintSeed = BigIntSeed.fromNumber(42);
const [bool1, newSeed1] = bigintSeed.nextBool(); // Individual operations
// Explicit WASM usage (fastest for computational workloads)
const wasmSeed = WasmSeed.fromNumber(42);
const [bool2, newSeed2] = wasmSeed.nextBool(); // Individual: 1.92x faster
const bulkBools = wasmSeed.nextBools(1000); // Batched: 18.37x faster
// Force BigInt even in AdaptiveSeed
const forcedBigInt = AdaptiveSeed.fromNumberBigInt(42);
All implementations support both individual operations and bulk operations, with WASM providing significant performance advantages for both single calls and batched operations.
AdaptiveSeed is the default Seed implementation that provides transparent optimization:
Use the default (AdaptiveSeed) - Recommended for 99% of use cases:
import { Seed } from 'hedgehog'; // This is AdaptiveSeed
const seed = Seed.fromNumber(42); // Automatically optimized
Use explicit BigInt when you need:
import { Seed as BigIntSeed } from 'hedgehog/seed/bigint';
const seed = BigIntSeed.fromNumber(42); // Pure JavaScript
Use explicit WASM when you need:
import { Seed as WasmSeed } from 'hedgehog/seed/wasm';
const seed = WasmSeed.fromNumber(42); // Pure WASM, no fallback
Create domain-specific generators:
// Email generator
const emailGen = Gen.tuple(
Gen.stringOfLength(Gen.int(3, 10)),
Gen.constant('@'),
Gen.stringOfLength(Gen.int(3, 8)),
Gen.constant('.com')
).map(([name, at, domain, tld]) => name + at + domain + tld);
// Tree structure generator
interface TreeNode {
value: number;
children: TreeNode[];
}
const treeGen: Gen<TreeNode> = Gen.sized(size => {
if (size.value <= 1) {
return Gen.object({
value: Gen.int(1, 100),
children: Gen.constant([])
});
}
return Gen.object({
value: Gen.int(1, 100),
children: Gen.array(treeGen.scale(s => s.scale(0.5)))
});
});
For performance-critical bulk generation:
const seed = Seed.fromNumber(42);
// Generate many booleans efficiently (uses automatic batching)
const boolResult = seed.nextBools(10000); // 18-128x faster than individual calls
console.log(boolResult.values.length); // 10000
console.log(boolResult.finalSeed); // Updated seed for further generation
// Generate many bounded values
const boundedResult = seed.nextBoundedBulk(5000, 100);
console.log(boundedResult.values.every(v => v >= 0 && v < 100)); // true
Configure test execution:
import { Config } from 'hedgehog';
const config = Config.default()
.withTestLimit(1000) // Run 1000 test cases
.withShrinkLimit(100) // Try up to 100 shrinking attempts
.withSeed(42); // Use specific seed for reproducibility
const result = property.check(config);
Based on comprehensive benchmarking on Apple M3 Pro:
The AdaptiveSeed automatically switches between implementations based on these characteristics, ensuring optimal performance for any workload.
If using WASM features and building from source:
Build WASM module:
npm run build:wasm
The library gracefully falls back to pure JavaScript if WASM is unavailable.
Run the test suite:
npm test # Run tests (fast)
npm run test:watch # Watch mode
Performance analysis:
npm run bench # Run performance benchmarks
Type checking and linting:
npm run typecheck # Type check
npm run lint # Lint code
npm run lint:fix # Fix lint issues
Contributions welcome! This library follows these principles:
TypeScript Hedgehog now includes comprehensive state machine testing capabilities, allowing you to test stateful systems with realistic command sequences:
import {
command, require, update, ensure,
sequential, forAllSequential, commandRange,
newVar, Gen, Range
} from 'hedgehog';
// Define your system's state
interface BankState {
accounts: Map<Variable<string>, { balance: number; isOpen: boolean }>;
}
// Create commands that model operations
const createAccount = command(
(_state) => Gen.object({ initialBalance: Gen.int(Range.uniform(0, 1000)) }),
async (input) => `account_${Math.random().toString(36).slice(2)}`,
require((_state, input) => input.initialBalance >= 0),
update((state, input, output) => ({
accounts: new Map(state.accounts).set(output, {
balance: input.initialBalance,
isOpen: true
})
})),
ensure((_before, after, _input, _output) => after.accounts.size > 0)
);
// Test realistic sequences of operations
const property = forAllSequential(
sequential(
commandRange(5, 15),
{ accounts: new Map() },
[createAccount, deposit, withdraw, closeAccount]
)
);
await property.check({ testLimit: 100 });
State machine testing provides:
See the State Machine Testing Guide for complete documentation.
BSD-3-Clause
FAQs
Property-based testing library for TypeScript, inspired by Hedgehog
We found that @justanotherdot/hedgehog demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.