Package detail

mimicry-js

Stivooo540MIT1.2.2

TS library for generating fake data for tests

testing, mock, mock-generator, fake-data

readme

mimicry-js

A lightweight and flexible TypeScript library for generating mock data for your tests with predefined structures, \ functional and iterable field generators, traits, and post-processing capabilities. \ It makes no assumptions about frameworks or libraries, and can be used with any test runner.

Mimicry-js was inspired by test-data-bot and offers more flexibility and advanced TypeScript support.

npm version

<summary>Table of Contents</summary> - Motivation - Installation - Usage - Basic Usage - Unique values - Built-in value generators - fixed - sequence - oneOf - bool - unique - withPrev - Resetting the state of sequence and unique - postBuild modifications and classes - Overrides per-build - Traits - Advanced features - Getting the entire result of the previous build - Using GeneratorFunction to create fields - Passing initialParameters - Using fields generators - Getting the result of the previous build - Plain object merging - Nested arrays - Custom iterators - Implementation of state reset - Best practices for using TypeScript types

Motivation

Rather than creating random objects each time you want to test something in your system you can instead use a builder that can create fake data. This keeps your tests consistent and means that they always use data that replicates the real thing. If your tests work off objects close to the real thing they are more useful and there's a higher chance of them finding bugs.

Installation

With npm:

npm install --save-dev mimicry-js

or using Yarn:

yarn add --dev mimicry-js

Usage

Basic Usage

Use the build function to create a builder. Just give a builder an object of fields you want to define:

import {build} from 'mimicry-js';

const builder = build({
    fields: {
        firstName: 'John',
        lastName: 'Doe',
    },
});

const profile = builder.one()

console.log(profile);
// { firstName: 'John', lastName: 'Doe' }

Once you've created a builder, you can call the one method it returns to generate an instance of that object - in this case, a profile.

You can also use the many method to create an array of instances of your object. The method expects a number indicating how many objects to generate.

const profiles = builder.many(3)

console.log(profiles);
// [
//     { firstName: 'John', lastName: 'Doe' },
//     { firstName: 'John', lastName: 'Doe' },
//     { firstName: 'John', lastName: 'Doe' }
// ]

Unique values

In the example above, you may notice that the objects returned by the builder are identical. This is not ideal for your tests, so mimicry-js allows you to use functions and iterators.

For example, a custom function that returns a single value from the set.

import {build} from 'mimicry-js';

// Returns one of the options
const getOneOf = <T>(options: T[]) => {
    return options[Math.floor(Math.random() * options.length)];
};

const builder = build({
    fields: {
        firstName: () => getOneOf(['John', 'Andrew']),
        lastName: 'Doe',
    },
});

const profiles = builder.many(2);

console.log(profiles);
// [
//     { firstName: 'John', lastName: 'Doe' },
//     { firstName: 'Andrew', lastName: 'Doe' }
// ]

The builder calls the specified function for the field when creating each instance.

In this case, the builder correctly infers the type for the field.

const profiles: {
    firstName: string
    lastName: string
}[]

So you can also use various external libraries to generate random values (e.g., Faker)

import {build} from 'mimicry-js';
import {faker} from '@faker-js/faker';

const builder = build({
    fields: {
        firstName: () => faker.person.firstName(),
        lastName: () => faker.person.lastName(),
    },
});

Built-in value generators

fixed

Since by default, the builder calls the provided functions to get field values, mimicry-js offers the fixed decorator, which allows keeping a value unchanged. \ For example, if we need the getName field in the generated object to be a function, we can wrap this function with fixed.

import {build, fixed} from 'mimicry-js';


const builder = build({
    fields: {
        type: 'plain',
        getName: fixed(() => 'Plain object'),
    },
});

const thing = builder.one();

console.log(thing.getName()); // --> "Plain object"

sequence

Often you will be creating objects that have an ID that comes from a database, so you need to guarantee that it's unique. You can use sequence, which increments the value on each call, starting from 1 (in versions ≤ "1.2.1" from 0):

import {build, sequence} from 'mimicry-js';

const profileBuilder = build({
    fields: {
        id: sequence(),
        firstName: 'John',
        lastName: 'Doe',
    },
});

const firstPerson = profileBuilder.one();
const secondPerson = profileBuilder.one();
const thirdPerson = profileBuilder.one();

// firstPerson.id === 1
// secondPerson.id === 2
// thirdPerson.id === 3

If you need more control, you can pass sequence a function that will be called with the number. This is useful to ensure completely unique emails, for example:

import {build, sequence} from 'mimicry-js';

const profileBuilder = build({
    fields: {
        firstName: 'John',
        lastName: 'Doe',
        email: sequence(x => `john${x}@mail.com`),
    },
});

const firstPerson = profileBuilder.one();
const secondPerson = profileBuilder.one();
const thirdPerson = profileBuilder.one();

// firstPerson.email === john1@mail.com
// secondPerson.email === john2@mail.com
// thirdPerson.email === john3@mail.com

oneOf

If you want an object to have a random value, picked from a list you control, you can use oneOf:

import {build, oneOf} from 'mimicry-js';

const userBuilder = build({
    fields: {
        name: oneOf(['John', 'Andrew', 'Mike']),
    },
});

const user = userBuilder.one();

// user.name === "John" | "Andrew" | "Mike"

bool

If you need something to be either true or false, you can use bool:

import {build, bool} from 'mimicry-js';

const userBuilder = build({
    fields: {
        isActive: bool(),
    },
});

const user = userBuilder.one();

// user.isActive === true | false

unique

Mimicry-js offers another one way to generate unique values. The unique function returns a single unique value from the provided set once on each call.

import {build, unique} from 'mimicry-js';

const userBuilder = build({
    fields: {
        firstName: unique(['John', 'Andrew', 'Mike']),
        lastName: 'Doe',
    },
});

const users = userBuilder.many(3);

console.log(users);

// [
//     { firstName: 'Andrew', lastName: 'Doe' },
//     { firstName: 'Mike', lastName: 'Doe' },
//     { firstName: 'John', lastName: 'Doe' }
// ]

userBuilder.one(); // throws Error "No unique options left!"

If there are no unused values left, unique throws an exception. Therefore, it's more appropriate to use this generator primarily in overrides to control the set of values.

withPrev

Sometimes we need unique but related values. For example, simulating the creation date of an entity. \ In such cases, you can use the withPrev decorator. It takes a function that has access to the result of the previous call of this function.

import {build, withPrev} from 'mimicry-js';

const userBuilder = build({
    fields: {
        name: 'John Doe',
        createdAt: withPrev((prevTimestamp?: number) => {
            const timestamp = prevTimestamp ?? new Date('2020').getTime();
            return timestamp + 1000;
        }),
    },
});

const firstUser = userBuilder.one();
const secondUser = userBuilder.one();
const thirdUser = userBuilder.one();

// firstUser.createdAt === 1577836801000
// secondUser.createdAt === 1577836802000
// thirdUser.createdAt === 1577836803000

Keep in mind that on the first call, the value will always be an undefined. \ Also, you need to inform the builder about the type of the received argument if a generic type is not specified for the builder itself.


The builder also supports passing nested plain objects with generators in fields.

Resetting the state of sequence and unique

In some cases, you may need to reset the state of the sequence and unique generators. To do this, you can call the builder.reset() method:

import {build, sequence, unique} from 'mimicry-js';

const builder = build({
    fields: {
        id: sequence(),
        name: unique('Sam', 'John', 'Mike'),
    },
});

const firstSet = builder.many(3);
console.log(firstSet);
// [
//     { id: 1, name: 'Sam' },
//     { id: 2, name: 'John' },
//     { id: 3, name: 'Mike' }
// ]

builder.reset();

const secondSet = builder.many(3);
console.log(secondSet);
// [
//     { id: 1, name: 'Sam' },
//     { id: 2, name: 'John' },
//     { id: 3, name: 'Mike' }
// ]

Additionally, you can implement state resetting in custom iterators.

postBuild modifications and classes.

Often, we need not just plain objects but instances of classes. In this case, you can pass a postBuild function along with fields. \ It allows you to transform the generated object as needed, for example, to create a class instance.

import {build} from 'mimicry-js';

class User {
    constructor(
        public id: number,
        public name: string,
    ) {}
}

const userBuilder = build({
    fields: {
        id: 1,
        firstName: 'John',
        lastName: 'Doe',
    },
    postBuild: ({id, firstName, lastName}) => new User(id, `${firstName} ${lastName}`),
});

const user = userBuilder.one();

console.log(user);
// User { id: 1, name: 'John Doe' }

In this case, the builder infers the ones return type as User.

const user: User

Overrides per-build

We often need to generate a random object but control one of the values directly for the purpose of testing. When you call a builder you can pass in overrides which will override the builder defaults:

import {build, sequence, oneOf} from 'mimicry-js';

const userBuilder = build({
    fields: {
        id: sequence(),
        name: oneOf('Sam', 'Andrew', 'Mike'),
    },
});

const user = userBuilder.one({
    overrides: {
        id: 5,
        name: 'John',
    },
});

console.log(user);
// { id: 5, name: 'John' }

If you need to edit the object directly, you can pass in a postBuild function when you call the builder. This will be called after Mimicry-js has generated the fake object, and lets you directly change it.

import {build, sequence, oneOf} from 'mimicry-js';

class User {
    constructor(
        public id: number,
        public name: string,
    ) {}
}

const userBuilder = build({
    fields: {
        id: sequence(),
        firstName: oneOf('Sam', 'Andrew', 'Mike'),
        lastName: oneOf('Doe', 'Smith', 'Jackson'),
    },
});

const user = userBuilder.one({
    overrides: {
        id: 5,
        firstName: 'John',
        lastName: 'Doe',
    },
    postBuild: ({id, firstName, lastName}) => new User(id, `${firstName} ${lastName}`),
});

console.log(user);
// User { id: 5, name: 'John Doe' }

In this case, the builder also determines that the return type of the one method has changed to User.

Using overrides and postBuild lets you easily customise a specific object that a builder has created.

Traits

Traits let you define a set of overrides for a builder that can easily be re-applied. Let's imagine you've got a users builder where users can have a support role and a certain email:

import {build, sequence, oneOf} from 'mimicry-js';

interface User {
    id: number;
    name: string;
    role: 'customer' | 'support' | 'administrator';
    email?: string;
}

const userBuilder = build<User>({
    fields: {
        id: sequence(),
        name: oneOf('John', 'Andrew', 'Mike'),
        role: oneOf('customer', 'support', 'administrator'),
    },
    traits: {
        support: {
            overrides: {
                role: 'support',
                email: 'support@mail.com',
            },
        },
    },
});

const support = userBuilder.one({traits: 'support'});

console.log(support);
// { id: 1, name: 'John', role: 'support', email: 'support@mail.com' }

Note that the support trait is specified above. As a result, the role and email fields will be overwritten on each call, and we don't have to do this manually using overrides:

const support = userBuilder.one({
    overrides: {
        role: 'support',
        email: 'support@mail.com',
    },
});

So now building a support user is easy:

const support = userBuilder.one({traits: 'support'});

In the example above, a generic type is used to specify the User type for simplicity. However, it is highly recommended to explore better alternatives for type specification in the Best Practices for Using TypeScript Types section.

Multiple traits

You can define and use multiple traits when building an object. Be aware that if two traits override the same value, the one passed in last wins:

import {build, sequence, oneOf} from 'mimicry-js';

interface User {
    id: number;
    name: string;
    role: 'customer' | 'support' | 'administrator';
    email?: string;
}

const userBuilder = build<User>({
    fields: {
        id: sequence(),
        name: oneOf('John', 'Andrew', 'Mike'),
        role: oneOf('customer', 'support', 'administrator'),
    },
    traits: {
        customer: {
            overrides: {
                role: 'customer',
            },
        },
        withContactDetails: {
            overrides: {
                email: 'contact@mail.com',
            },
        },
    },
});

const customer = userBuilder.one({traits: ['customer', 'withContactDetails']});

console.log(customer);
// { id: 1, name: 'John', role: 'customer', email: 'contact@mail.com' }

You can use overrides together with traits. In this case, values from overrides will override the corresponding ones from traits.

Advanced features

Getting the entire result of the previous build

Sometimes we need to generate complex objects with related values. In this case, the builder allows passing fields as a function that returns an object to build and takes the result of the previous call.

For example, in the code below, the price field depends on the value of the count field. Moreover, the count field changes with each build, so we need access to the result of the previous call.

import {build, sequence} from 'mimicry-js';

interface Order {
    count: number;
    price: number;
}

const orderBuilder = build({
    fields: (previous?: Order) => {
        const count = previous ? previous.count + 1 : 1;

        return {
            count: sequence(),
            price: 1000 * count,
        };
    },
});

const orders = orderBuilder.many(3);

console.log(orders);
// [
//     { count: 1, price: 1000 },
//     { count: 2, price: 2000 },
//     { count: 3, price: 3000 }
// ]

Note that the value of the previous build will always be undefined on the first call. \ Also, you need to inform the builder about the type of the received argument if a generic type is not specified for the builder itself.

The builder preserves iterators after the first function call and continues using them instead of creating new ones, even though the function passed as fields is called each time.

Using GeneratorFunction to create fields

If you need more control when creating objects, you can use generator functions. This allows you to define and manage the generation logic at each iteration.

For this, Mimicry-js provides the generate decorator, which expects a generator function as an argument:

import {build, generate} from 'mimicry-js';

function* timePeriodsGenerator() {
    let currentStart = new Date('2025-01-01').getTime();
    const periodDurationHs = 24;
    const periodDurationMs = periodDurationHs * 60 * 60 * 1000; // Hours value in milliseconds

    while (true) {
        const currentEnd = currentStart + periodDurationMs;
        yield {start: new Date(currentStart), end: new Date(currentEnd)};
        currentStart = currentEnd;
    }
}

const builder = build({
    fields: generate(timePeriodsGenerator),
});

const periods = builder.many(3);

console.log(periods);
// [
//     { start: 2025-01-01T00:00:00.000Z, end: 2025-01-02T00:00:00.000Z },
//     { start: 2025-01-02T00:00:00.000Z, end: 2025-01-03T00:00:00.000Z },
//     { start: 2025-01-03T00:00:00.000Z, end: 2025-01-04T00:00:00.000Z }
// ]

In this case, you can also use overrides and traits.

It is important to note that only infinite generators are supported.

The provided generator function is called each time the many or one methods are called. This means that each build will be independent of the others. This is necessary to prevent unrelated tests from affecting each other:

const builder = build({
    fields: generate(timePeriodsGenerator),
});

const firstPeriodsSet = builder.many(3);
const secondPeriodsSet = builder.many(3);

console.log(firstPeriodsSet);
// [
//     { start: 2025-01-01T00:00:00.000Z, end: 2025-01-02T00:00:00.000Z },
//     { start: 2025-01-02T00:00:00.000Z, end: 2025-01-03T00:00:00.000Z },
//     { start: 2025-01-03T00:00:00.000Z, end: 2025-01-04T00:00:00.000Z }
// ]

console.log(secondPeriodsSet);
// [
//     { start: 2025-01-01T00:00:00.000Z, end: 2025-01-02T00:00:00.000Z },
//     { start: 2025-01-02T00:00:00.000Z, end: 2025-01-03T00:00:00.000Z },
//     { start: 2025-01-03T00:00:00.000Z, end: 2025-01-04T00:00:00.000Z }
// ]

This does not apply to nested field generators, which are preserved after the first generation.

Passing initialParameters to the generator function

It can be very useful to pass some initial values to the generator function at the moment of object generation. \ So, BuildTimeConfig has an optional initialParameters field, which accepts a tuple of arguments taken by the generator function:

import {build, generate} from 'mimicry-js';

function* timePeriodsGenerator(currentStartDate: Date, periodDurationInMs: number) {
    let currentStart = currentStartDate.getTime();

    while (true) {
        const currentEnd = currentStart + periodDurationInMs;
        yield {start: new Date(currentStart), end: new Date(currentEnd)};
        currentStart = currentEnd;
    }
}

const builder = build({
    fields: generate(timePeriodsGenerator),
});

const start = new Date('2025-01-01');
const duration = 24 * 60 * 60 * 1000;

const periods = builder.many(3, {
    initialParameters: [start, duration],
});

console.log(periods);
// [
//     { start: 2025-01-01T00:00:00.000Z, end: 2025-01-02T00:00:00.000Z },
//     { start: 2025-01-02T00:00:00.000Z, end: 2025-01-03T00:00:00.000Z },
//     { start: 2025-01-03T00:00:00.000Z, end: 2025-01-04T00:00:00.000Z }
// ]

With generate, the builder can validate the types of initialParameters.

// TS2322: Type [] is not assignable to type
// [currentStartDate: Date, periodDurationInMs: number]
// Source has 0 element(s) but target requires 2
const periods = builder.many(3, {
  initialParameters: [],
});

Using fields generators

You can still use generators and functions to create field values:

import {build, generate, oneOf} from 'mimicry-js';

function* timePeriodsGenerator(currentStartDate: Date, periodDurationInMs: number) {
    let currentStart = currentStartDate.getTime();

    while (true) {
        const currentEnd = currentStart + periodDurationInMs;
        yield {
            id: sequence(),
            start: new Date(currentStart),
            end: new Date(currentEnd),
            type: oneOf('open', 'closed')
        };
        currentStart = currentEnd;
    }
}

const builder = build({
    fields: generate(timePeriodsGenerator),
});

const start = new Date('2025-01-01');
const duration = 24 * 60 * 60 * 1000;

const periods = builder.many(3, {
    initialParameters: [start, duration],
});

console.log(periods);
// [
//     {
//         id: 1,
//         start: 2025-01-01T00:00:00.000Z,
//         end: 2025-01-02T00:00:00.000Z,
//         type: 'open'
//     },
//     {
//         id: 2,
//         start: 2025-01-02T00:00:00.000Z,
//         end: 2025-01-03T00:00:00.000Z,
//         type: 'closed'
//     },
//     {
//         id: 3,
//         start: 2025-01-03T00:00:00.000Z,
//         end: 2025-01-04T00:00:00.000Z,
//         type: 'open'
//     }
// ]

The builder preserves iterators after the first generator function iteration and continues using them instead of creating new ones.

Getting the result of the previous build

You can also get the result of the previous build:

import {build, generate} from 'mimicry-js';

type Period = {
    start: Date;
    end: Date;
};

function* timePeriodsGenerator(currentStartDate: Date, periodDurationInMs: number) {
    let currentStart = currentStartDate.getTime();

    while (true) {
        const previousBuildResult: Period = yield {
            start: new Date(currentStart),
            end: new Date(currentStart + periodDurationInMs),
        };
        currentStart = previousBuildResult.end.getTime();
    }
}

const builder = build({
    fields: generate(timePeriodsGenerator),
});

const start = new Date('2025-01-01');
const duration = 24 * 60 * 60 * 1000;

const periods = builder.many(3, {
    initialParameters: [start, duration],
});

console.log(periods);
// [
//     { start: 2025-01-01T00:00:00.000Z, end: 2025-01-02T00:00:00.000Z },
//     { start: 2025-01-02T00:00:00.000Z, end: 2025-01-03T00:00:00.000Z },
//     { start: 2025-01-03T00:00:00.000Z, end: 2025-01-04T00:00:00.000Z }
// ]

You need to specify the type of the value received via yield manually.

Deep plain object merging in overrides and traits

Let's imagine that one of the object's fields is another object that also requires fake data. The builder supports using field generators in nested objects:

import {build, sequence, oneOf} from 'mimicry-js';

interface Account {
    id: number;
    name: string;
    address: {
        apartment: string;
        street: string;
        city: string;
        postalCode: number;
    };
}

const builder = build<Account>({
    fields: {
        id: sequence(),
        name: 'John',
        address: {
            apartment: sequence((x) => x.toString()),
            street: oneOf('123 Main St', '456 Elm Ave'),
            city: oneOf('New York', 'Los Angeles'),
            postalCode: sequence((x) => x + 1000),
        },
    },
});

const account = builder.one();

console.log(account);
// {
//   id: 1,
//   name: 'John',
//   address: {
//     apartment: '1',
//     street: '456 Elm Ave',
//     city: 'Los Angeles',
//     postalCode: 1000
//   }
// }

You can just as easily create a separate builder for the address object and use it, but in this case, the data will be static.

Note that in this case, you must specify the type to ensure the builder correctly infers types. However, it is highly recommended to explore better alternatives for type specification in the Best Practices for Using TypeScript Types section.

When using this builder, we may need to override certain fields, such as city and street of the address. So, we can do that:

const account = builder.one({
    overrides: {
        address: {
            city: 'San Francisco',
            street: '101 Pine Ln',
        },
    },
});

console.log(account);
// {
//   id: 1,
//   name: 'John',
//   address: {
//     apartment: '1',
//     street: '101 Pine Ln',
//     city: 'San Francisco',
//     postalCode: 1000
//   }
// }

You may notice that we don't need to specify all the fields of the address object in overrides. This behavior is also similar for traits:

const builder = build<Account>({
    fields: {
        id: sequence(),
        name: 'John',
        address: {
            apartment: sequence((x) => x.toString()),
            postalCode: sequence((x) => x + 1000),
            street: '',
            city: '',
        },
    },
    traits: {
        NY: {
            overrides: {
                address: {
                    street: '123 Main St',
                    city: 'New York',
                },
            },
        },
        LA: {
            overrides: {
                address: {
                    street: '456 Elm Ave',
                    city: 'Los Angeles',
                },
            },
        },
    },
});

const account = builder.one({
    traits: 'LA',
});

console.log(account);
// {
//   id: 1,
//   name: 'John',
//   address: {
//     apartment: '1',
//     postalCode: 1000,
//     street: '456 Elm Ave',
//     city: 'Los Angeles'
//   }
// }

Nested arrays of configurations with field generators

The builder checks the values of arrays in the provided fields to handle nested generators.

import {build, sequence, oneOf} from 'mimicry-js';

interface Account {
    id: number;
    name: string;
    addresses: Array<{
        apartment: string;
        street: string;
        city: string;
        postalCode: number;
    }>;
}

const builder = build<Account>({
    fields: {
        id: sequence(),
        name: 'John',
        addresses: [],
    },
});

const account = builder.one({
    overrides: {
        addresses: [
            {
                apartment: sequence((x) => x.toString()),
                street: oneOf('456 Elm Ave'),
                city: oneOf('Los Angeles'),
                postalCode: 98101,
            },
            {
                apartment: sequence((x) => x.toString()),
                street: oneOf('101 Pine Ln'),
                city: oneOf('San Francisco'),
                postalCode: 10001,
            },
        ],
    },
});

console.log(account);
// {
//   id: 1,
//   name: 'John',
//   addresses: [
//     {
//       apartment: '1',
//       street: '456 Elm Ave',
//       city: 'Los Angeles',
//       postalCode: 98101
//     },
//     {
//       apartment: '1',
//       street: '101 Pine Ln',
//       city: 'San Francisco',
//       postalCode: 10001
//     }
//   ]
// }

However, the builder does not perform deep merging of arrays in traits and overrides.

Note that in this case, you must specify the type to ensure the builder correctly infers types. However, it is highly recommended to explore better alternatives for type specification in the Best Practices for Using TypeScript Types section.

Custom iterators

You can also use custom iterators to generate field values:

import {build} from 'mimicry-js';

function* exponentiation(initialValue: number) {
    let exponent = 0;

    while (true) {
        yield initialValue ** ++exponent;
    }
}

const builder = build({
    fields: {
        exponent: exponentiation(2),
    },
});

const [first, second, third] = builder.many(3);

// first.exponent === 2
// second.exponent === 4
// third.exponent === 8

Keep in mind that only infinite generators are supported.

Implementation of state reset

The builder can reset the state of sequence and unique by calling the builder.reset() method. You can track this method call to reset values in your custom generator function.

To facilitate this, Mimicry-js provides the resetable utility, which allows managing state within a generator. It takes an initial value and returns a Resetable instance with three methods: val, set, and use.

  • val provides access to the current state.
  • set allows updating the state; it takes a new value and returns the updated one.
  • use subscribes a specific Resetable instance to ResetSignal.

In the example below, when builder.reset() is called, the val state resets to its initial value, which in this case is zero:

import {build, resetable} from 'mimicry-js';

function* exponentiation(initialValue: number) {
    const {val, set, use} = resetable(0);

    while (true) {
        use(yield initialValue ** set(val() + 1));
    }
}

const builder = build({
    fields: {
        exponent: exponentiation(2),
    },
});

const firstSet = builder.many(3);
console.log(firstSet); // [ { exponent: 2 }, { exponent: 4 }, { exponent: 8 } ]

builder.reset();

const secondSet = builder.many(3);
console.log(secondSet); //  [ { exponent: 2 }, { exponent: 4 }, { exponent: 8 } ]

This example may seem somewhat complex to understand. However, there is no "magic" happening here.

If you take a closer look at the exponentiation type inferred by TypeScript in the example above, you'll see that the Generator accepts ResetSignal as the TNext generic type.

function exponentiation(initialValue: number): Generator<number, void, ResetSignal>

When the builder calls the next() method on your iterator, it passes an instance of ResetSignal as an argument, which is then returned by the yield operator inside the generator function.

This example could be rewritten in a more explicit form, but in that case, you would need to define ResetSignal type definition yourself.

import {build, resetable, ResetSignal} from 'mimicry-js';

function* exponentiation(initialValue: number) {
    const {val, set, use} = resetable(0);

    while (true) {
        const exponent = val() + 1;
        set(exponent);

        const signal: ResetSignal = yield initialValue ** exponent;

        use(signal);
    }
}

const builder = build({
    fields: {
        exponent: exponentiation(2),
    },
});

const firstSet = builder.many(3);
console.log(firstSet); // [ { exponent: 2 }, { exponent: 4 }, { exponent: 8 } ]

builder.reset();

const secondSet = builder.many(3);
console.log(secondSet); //  [ { exponent: 2 }, { exponent: 4 }, { exponent: 8 } ]

To avoid errors, try to implement your generator function in such a way that all state updates are performed before returning a value using the yield operator in an infinite loop. \ If you update the state after returning from yield, the code will resume execution right after yield in the next iteration. This means that immediately after resetting the state, you will update it again, causing the val value in the next while loop iteration to differ from its initial value (the one passed to resetable).

import {build, resetable} from 'mimicry-js';

function* exponentiation(initialValue: number) {
    const {val, set, use} = resetable(1);

    while (true) {
        const exponent = val() + 1;
        use(yield initialValue ** exponent);
        set(exponent);
    }
}

const builder = build({
    fields: {
        exponent: exponentiation(2),
    },
});

const firstSet = builder.many(3);
console.log(firstSet); // [ { exponent: 2 }, { exponent: 4 }, { exponent: 8 } ]

builder.reset();

const secondSet = builder.many(3);
console.log(secondSet); //  [ { exponent: 32 }, { exponent: 64 }, { exponent: 128 } ]

The state was not reset because it was updated with exponent from the previous iteration!

Best practices for using TypeScript types

Mimicry-js is written in TypeScript and ships with the types generated so if you're using TypeScript you will get type support out the box. \ The builder below, in addition to the object with fields, has a set of traits and a postBuild transformer.

import {build} from 'mimicry-js';

class Profile {
    name: string;
    age?: number;

    constructor({firstName, lastName, age}: IProfileData) {
        this.name = `${firstName} ${lastName}`;
        this.age = age;
    }
}

const profile = {
    firstName: 'John',
    lastName: 'Doe',
    age: 30,
};

const builder = build({
    fields: profile,
    traits: {
        younger: {
            overrides: {
                age: 18,
            },
        },
        older: {
            overrides: {
                age: 50,
            },
        },
    },
    postBuild: (generatedFields) => new Profile(generatedFields),
});

And it has all the information about the input data types, trait names, and the result type:

const builder: Builder<{
    firstName: string
    lastName: string
    age: number
}, Profile, "younger" | "older", never>

The never type at the end indicates the type of initialParameters when using GeneratorFunction to create fields.

So, in most situations, types don’t need to be specified manually, except for cases with generators in nested objects and arrays.

If you manually specify the builder object's generic, the builder loses information about the specific traits names (the type becomes string) and initialParameters (the type becomes never). This is due to TypeScript's behavior: default values are used for all generics if even one of them is provided.

const builder = build<Profile>({ ... });
const builder: Builder<Profile, Profile, string, never>

So, if you want to get type checking and use type-based code suggestions when filling out fields, while allowing the builder to infer types automatically, you can use the built-in FieldsConfiguration type:

import {build, sequence, FieldsConfiguration} from 'mimicry-js';

interface IProfileData {
    id: number;
    firstName: string;
    lastName: string;
    age?: number;
}

const profile: FieldsConfiguration<IProfileData> = {
    id: sequence(),
    firstName: 'John',
    lastName: 'Doe',
    age: 30,
};

const builder = build({
    fields: profile,
    postBuild: (generatedFields) => new Profile(generatedFields),
    traits: {
        younger: {
            overrides: {
                age: 18,
            },
        },
        older: {
            overrides: {
                age: 50,
            },
        },
    },
});

As a result, the builder retains all the type information and can validate the trait names passed to it, while you get all the TypeScript checks when filling out the fields.

const builder: Builder<IProfileData, Profile, "younger" | "older", never>
builder.one({
    traits: 'other'
})

// TS2322: Type "other" is not assignable to type
// "younger" | "older" | ("younger" | "older")[] | undefined

Similarly, in the case of using GeneratorFunction:

import {build, withPrev, FieldsConfiguration} from 'mimicry-js';

interface IProfileData {
    id: number;
    firstName: string;
    lastName: string;
    age?: number;
}

const builder = build({
    fields: generate(function* (startId: number) {
        while (true) {
            const profile: FieldsConfiguration<IProfileData> = {
                id: withPrev((previous) => (previous ? previous + 1 : startId)),
                firstName: 'John',
                lastName: 'Doe',
                age: 30,
            };

            yield profile;
        }
    }),
    postBuild: (generatedFields) => new Profile(generatedFields),
    traits: {
        younger: {
            overrides: {
                age: 18,
            },
        },
        older: {
            overrides: {
                age: 50,
            },
        },
    },
});

As a result:

const builder: Builder<IProfileData, Profile, "younger" | "older", [startId: number]>

License

MIT

changelog

Change Log

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

[Unreleased]

[1.2.2] - 2025-03-31

Fixed

  • Change the start value of sequence to 1.

[1.2.1] - 2025-03-31

Fixed

  • Fix exports of resetable and ResetSignal.

[1.2.0] - 2025-03-31

Fixed

  • Fix the typing of postBuild in BuildTimeConfig when postBuild is present in the BuilderConfiguration.

Added

[1.1.1] - 2025-03-28

Fixed

  • Fix the behavior of builders using generator functions: the generator should be initialized on each call of the one and many methods.

[1.1.0] - 2025-03-26

Fixed

  • Add default type value for TraitName in TraitsConfiguration

Added

[1.0.2] - 2025-03-22

Fixed

  • Fix the release.yml CI for correct archive building and README update.

[1.0.1] - 2025-03-22

Fixed

  • Just fix the CI workflow to update the README before publishing.

[1.0.0] - 2025-03-21

Initial release

  • First stable version of the library. Check out the README.