@wluwd/variations
v1.0.0
Published
Generate all possible variations of object properties using cartesian products
Readme
@wluwd/variations
Generate all possible variations of object properties using cartesian products
A lightweight, type-safe utility for generating all combinations of object properties. Perfect for testing component variants, creating configuration matrices, or any scenario where you need exhaustive combinations.
Features
- 📦 Type-safety: full TypeScript support with precise type inference
- 🪶 Lightweight: zero dependencies, ESM-only, minimal bundle size
- 🏎️ Efficient: memory-efficient lazy evaluation for large datasets
- 🌐 Universal: works in LTS Node.js (20+) and modern browsers
Installation
[!WARNING] This package is ESM only. Ensure your project uses
"type": "module"inpackage.json.
npm install @wluwd/variationspnpm add @wluwd/variationsyarn add @wluwd/variationsThe Problem
When testing UI components with multiple props, manually writing test cases becomes unsustainable.
// Manual approach - error-prone and difficult and exhausting to maintain
it('renders primary small button', () => { /* ... */ })
it('renders primary normal button', () => { /* ... */ })
it('renders primary large button', () => { /* ... */ })
it('renders secondary small button', () => { /* ... */ })
// ... many more tests to write by handWhat if there's a better way though? That's where this library comes into play:
// Automated approach - declarative and maintainable
const testCases = eagerVariations({
variant: ['primary', 'secondary', 'destructive'],
size: ['small', 'normal', 'large'],
status: ['idle', 'loading', 'disabled']
});
it.for(testCases)('Button: %o', (props) => { /* ... */ });
// ✨ all tests generated automaticallyQuick Start
Basic Usage
import { eagerVariations } from '@wluwd/variations';
const configs = eagerVariations({
color: ['red', 'blue', 'green'],
size: ['small', 'large']
});
console.log(configs);
// [
// { color: 'red', size: 'small' },
// { color: 'red', size: 'large' },
// { color: 'blue', size: 'small' },
// { color: 'blue', size: 'large' },
// { color: 'green', size: 'small' },
// { color: 'green', size: 'large' }
// ]Type-Safe Factory
For better autocomplete and output type inference, use defineVariations:
import { defineVariations } from '@wluwd/variations';
interface ButtonProps {
variant: 'primary' | 'secondary';
size: 'small' | 'large';
}
const { eager, lazy } = defineVariations<ButtonProps>();
// 🪄 Full autocomplete with inferred output type:
// { variant: 'primary'; size: 'small' | 'large' }
const variations = eager({
variant: ['primary'],
size: ['small', 'large']
});With Filtering
const validConfigs = eagerVariations(
{
variant: ['primary', 'link'],
size: ['small', 'large']
},
{
// Skip invalid combinations
filter: v => !(v.variant === 'link' && v.size === 'large')
}
);Memory-Efficient Processing
For large datasets, use lazyVariations to process one variation at a time:
import { lazyVariations } from '@wluwd/variations';
for (const config of lazyVariations({
variant: ['primary', 'secondary', 'destructive'],
size: ['small', 'normal', 'large'],
status: ['idle', 'loading', 'disabled']
})) {
// Process each of 27 variations without loading all into memory
await processConfig(config);
}API
eagerVariations(base, options?)
Generates all variations and returns them as an array.
Parameters:
base- Object where each property is an array of possible valuesoptions?- Optional configuration objectfilter?- Function to filter which variations to includesafe?- Iftrue, returns empty array for invalid input instead of throwing
Returns: Array of all variation objects
lazyVariations(base, options?)
Generates variations one at a time using a generator.
Parameters: Same as eagerVariations
Yields: Individual variation objects
defineVariations<T>()
Creates a type-safe factory bound to a specific interface. It returns an object with two methods: eager and lazy. These work the same way as their stand-alone counterparts.
interface ButtonProps {
variant: 'primary' | 'secondary';
size: 'small' | 'large';
}
const { eager } = defineVariations<ButtonProps>();
// 🪄 `eager` provides autocomplete for properties
const variations = eager({
variant: ['primary'],
size: ['small', 'large']
});
// typeof variations → Array<{ variant: 'primary', size: 'small' | 'large' }>VariationsInput<T>
Type helper for explicitly typing variation inputs.
interface ButtonProps {
variant: 'primary' | 'secondary';
size: 'small' | 'large';
}
const input = {
variant: ['primary', 'secondary'],
size: ['small']
} satisfies VariationsInput<ButtonProps>;This type is useful when directly using eagerVariations or lazyVariations as it allows to get the same output type as the helpers bound by defineVariations:
interface ButtonProps {
variant: 'primary' | 'secondary';
size: 'small' | 'large';
}
const variations = eagerVariations({
variant: ['primary'],
size: ['small', 'large']
} satisfies VariationsInput<ButtonProps>);
// typeof variations → Array<{ variant: 'primary', size: 'small' | 'large' }>Real-World Example
Visual regression testing for a design system:
import { eagerVariations } from '@wluwd/variations';
import { render } from '@testing-library/react';
interface ButtonProps {
variant: 'primary' | 'secondary' | 'destructive' | 'link';
size: 'small' | 'normal' | 'large';
status?: 'loading' | 'disabled';
}
describe('Button visual regression', () => {
const cases = eagerVariations({
variant: ['primary', 'secondary', 'destructive', 'link'],
size: ['small', 'normal', 'large'],
status: [undefined, 'loading', 'disabled']
} satisfies VariationsInput<ButtonProps>);
it.for(cases)('matches snapshot: %o', async (props) => {
const screen = render(<Button {...props}>Click me</Button>);
// Test default state
expect(screen.getButton()).toMatchSnapshot();
// Test hover state
await userEvent.hover(screen.getButton());
expect(screen.getButton()).toMatchSnapshot();
// Test active state
await userEvent.click(screen.getButton());
expect(screen.getButton()).toMatchSnapshot();
});
});
// ✨ Generates 108 comprehensive tests automaticallyBackground
This library was born from the need to test design system components exhaustively without manual overhead. What started as checking a handful of button variants eventually grew to 120+ combinations that needed verification for every CSS change.
Read the full story: When Manual Testing Becomes Unsustainable.
Performance
The algorithm uses an odometer/mixed-radix counter approach that:
- Only updates changed dimensions between iterations
- Maintains stable object shapes for optimization
- Supports lazy evaluation to avoid loading all combinations into memory
Keys are processed left-to-right, with leftmost keys being most stable (changing least frequently).
License
MIT
