Skip to content

Instantly share code, notes, and snippets.

@JonCatmull
Last active January 8, 2025 17:03
Show Gist options
  • Save JonCatmull/88bc8bbf96ea91592229dfc0aa27f416 to your computer and use it in GitHub Desktop.
Save JonCatmull/88bc8bbf96ea91592229dfc0aa27f416 to your computer and use it in GitHub Desktop.
Build mock data easily
/**
* A `CompositionFunction` creates or updates one specific property on
* your mock data object. It returns a new `Builder` instance with that
* updated property value.
*
* @example
* // Suppose we have a builder for a User object
* // The `.username('SteveRogers')` is a composition function call:
* const steve = UserBuilder.username('SteveRogers').build();
*/
export type CompositionFunction<T, K extends keyof T> = (value: T[K]) => Builder<T>;
/**
* `CompositionFunctions<T>` is a dictionary of composition functions
* (one per property in `T`).
*
* Example: If T = { id: number; username: string },
* this type has `id(value: number): Builder<T>` and `username(value: string): Builder<T>`.
*/
export type CompositionFunctions<T> = Required<{
[P in keyof T]: CompositionFunction<T, P>;
}>;
/**
* A helper type for the `buildMany` function, which can create an array of objects
* either from an array of partial overrides or by a numeric count of duplicates.
*/
export type BuildManyFunction<T> = (arg: Partial<T>[] | number) => T[];
/**
* A `Builder<T>` includes:
* - One composition function per property in `T`
* - A `_obj` property containing the current base/default object
* - A `.build()` method returning a finalized, read-only object
* - A `.buildMany()` method to produce an array of objects in bulk
*/
export type Builder<T> = CompositionFunctions<T> & {
/**
* Read-only property with the “current” object state.
*/
_obj: Readonly<T>;
/**
* Builds a single object from the current default/base values
* plus any overridden properties set via composition functions.
*
* @returns A read-only copy of the final object.
*/
build(): Readonly<T>;
/**
* Builds an array of objects.
* - Pass an array of partial overrides to merge with the default/base object
* - Pass a number N to create N identical copies of the default/base object
*
* @example
* const multipleUsers = UserBuilder.buildMany([
* { username: 'BruceBanner' },
* { username: 'NatashaRomanoff' }
* ]);
*/
buildMany: BuildManyFunction<T>;
};
/**
* `CreateBuilderFunction` is a function type that takes a default value `T`
* and returns a `Builder<T>`.
*
* @example
* // Usage:
* export const UserBuilder: Builder<User> = createBuilder<User>(myDefaultUser);
*/
export type CreateBuilderFunction = <T>(defaultValue: T) => Builder<T>;
/**
* Creates a composition function for a specific property (keyName) on `obj`.
* When invoked, it sets that property to the provided `value` and returns a new `Builder`.
*
* @param builderFunction The top-level factory function (createBuilder).
* @param obj The current mock data object.
* @param keyName The property name to set (e.g., 'username').
*/
function createCompositionFunction<T, K extends keyof T>(
builderFunction: CreateBuilderFunction,
obj: T,
keyName: K
): CompositionFunction<T, K> {
return (value: T[K]) => builderFunction<T>({ ...obj, [keyName]: value });
}
/**
* Dynamically creates a dictionary of composition functions for each property in `obj`.
* For every key in `obj`, we create a function that sets that key and returns a new builder.
*
* @param builderFunction The top-level factory function (createBuilder).
* @param obj The current mock data object.
* @returns A `CompositionFunctions<T>` object where each property is a function.
*/
function createCompositionFunctions<T extends {}>(
builderFunction: CreateBuilderFunction,
obj: T
): CompositionFunctions<T> {
const compositionFuncs: Partial<CompositionFunctions<T>> = {};
// We use (keyof T)[] to assure TS that every key is truly a key of T
for (const key of Object.keys(obj) as (keyof T)[]) {
compositionFuncs[key] = createCompositionFunction(builderFunction, obj, key);
}
return compositionFuncs as CompositionFunctions<T>;
}
/**
* Creates a `.buildMany()` function for a given object. It can either:
* - Take an array of partial overrides and return an array of merged objects
* - Take a number N and return an array of N identical copies of the object
*
* @param obj The current mock data object.
* @returns A function that implements `.buildMany()`.
*/
function createBuildManyFunction<T>(obj: T): BuildManyFunction<T> {
return (arg: Partial<T>[] | number) => {
if (Array.isArray(arg)) {
return arg.map((partialObj) => ({
...obj,
...partialObj,
}));
}
// If arg is a number, return an array with `arg` copies of the object
return Array(arg).fill({ ...obj });
};
}
/**
* The main builder factory. Given a default object of type `T`,
* it returns a `Builder<T>` with composition methods for each property,
* plus `.build()` and `.buildMany()`.
*
* @param defaultValue The base/default object for your mock data.
*
* @example
* // Example usage with the `User` interface:
* import { User } from './user.interface';
*
* const defaultUser: User = {
* id: 3000,
* username: 'TonyStark',
* email: '[email protected]',
* roleIds: [999],
* roleId: '999',
* depotIds: ['STARK_TOWER'],
* sso: false,
* };
*
* export const UserBuilder: Builder<User> = createBuilder<User>(defaultUser);
*
* // Then in tests:
* const singleUser = UserBuilder.username('SteveRogers').build();
* const multipleUsers = UserBuilder.buildMany([
* { username: 'BruceBanner' },
* { username: 'NatashaRomanoff', email: '[email protected]' },
* ]);
*/
export const createBuilder: CreateBuilderFunction = <T>(defaultValue: T): Builder<T> => {
// Make a read-only copy of the default object
const _obj: Readonly<T> = defaultValue;
return {
_obj,
build: () => _obj,
buildMany: createBuildManyFunction(_obj),
...createCompositionFunctions(createBuilder, _obj),
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment