Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

readme.md

@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" in package.json.

npm install @wluwd/variations
pnpm add @wluwd/variations
yarn add @wluwd/variations

The 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 hand

What 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 automatically

Quick 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 values
  • options? - Optional configuration object
    • filter? - Function to filter which variations to include
    • safe? - If true, 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 automatically

Background

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