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.
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
one
s return type asUser
.
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 toUser
.
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 fromoverrides
will override the corresponding ones fromtraits
.
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 ofinitialParameters
.
// 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
andoverrides
.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 specificResetable
instance toResetSignal
.
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 ofResetSignal
as an argument, which is then returned by theyield
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 fromyield
, the code will resume execution right afteryield
in the next iteration. This means that immediately after resetting the state, you will update it again, causing theval
value in the nextwhile
loop iteration to differ from its initial value (the one passed toresetable
).
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 ofinitialParameters
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 becomesstring
) andinitialParameters
(the type becomesnever
). 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