Skip to content

nemmtor/ts-databuilders

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

341 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🧱 TS DataBuilders

Automatically generate type-safe builder classes from your TypeScript types to write cleaner, more focused tests.

Installation

Install the package:

# npm
npm install -D @nemmtor/ts-databuilders

# pnpm
pnpm add -D @nemmtor/ts-databuilders

# yarn
yarn add -D @nemmtor/ts-databuilders

Configuration

Configuration is optional - it fallbacks to sensible defaults.

Configure via CLI flags (optional):

pnpm ts-databuilders --include "src/**/*.ts{,x}" --output-dir src/__generated__ --builder-jsdoc-tag-name DataBuilder

You can also provide configuration by going through interactive wizard:

pnpm ts-databuilders --wizard

Configure via config file (optional)

Ts-databuilders will try to find config file ts-databuilders.json in the root of your repository. Config file is optional.

Example of default config file:

{
  "$schema": "https://raw.githubusercontent.com/nemmtor/ts-databuilders/refs/heads/main/schema.json",
  "builderJsDocTagName": "DataBuilder",
  "inlineDefaultJsDocTagName": "DataBuilderDefault",
  "withNestedBuilders": true,
  "outputDir": "generated/builders",
  "include": "src/**/*.ts{,x}",
  "fileSuffix": ".builder",
  "fileCase": "kebab",
  "builderSuffix": "Builder",
  "defaults": {
    "string": "",
    "number": 0,
    "boolean": false
  }
}

You can generate a default configuration file by running init command:

pnpm ts-databuilders init

You can also generate it by providing values step by step in an interactive wizard:

pnpm ts-databuilders init --wizard

Options Reference

Name (in config file) Flag (cli flags) Description Default
tsconfig --tsconfig -t Path to tsconfig file tsconfig.json
builderJsDocTagName --builder-jsdoc-tag-name JSDoc tag to mark types for generation DataBuilder
inlineDefaultJsDocTagName --inline-default-jsdoc-tag-name JSDoc tag used to set default value of given field DataBuilderDefault
withNestedBuilders --with-nested-builders When set to true ts-databuilders will use nested builders approach true
outputDir --output-dir -o Output directory for generated builders generated/builders
include --include -i Glob pattern for source files src/**/*.ts{,x}
fileSuffix --file-suffix File suffix for builder files .builder
fileCase --file-case Naming convention for generated builder file, one of 3: kebab, camel, pascal kebab
builderSuffix --builder-suffix Class name suffix Builder
defaults --default-string --default-number --default-boolean Default values for primitives See example above

Priority: CLI flags > Config file > Built-in defaults

Debugging

In order to turn on debug logs pass a flag: --log-level debug.

TSConfig References

If your project uses multiple tsconfig files, point ts-databuilders to the one that includes your source files in its include field.

pnpm ts-databuilders --tsconfig tsconfig.app.json

Or in config file:

{
  "tsconfig": "tsconfig.app.json"
}

Quick Start

1. Annotate your types with JSDoc:

/**
 * @DataBuilder
 */
type User = {
  id: string;
  email: string;
  name: string;
  isActive: boolean;
}

2. Generate builders:

pnpm ts-databuilders

For the User type above, you'll get:

import type { User } from "...";
import { DataBuilder } from "./data-builder";

export class UserBuilder extends DataBuilder<User> {
    constructor() {
        super({
          id: "",
          email: "",
          name: "",
          isActive: false
        });
    }

    withId(id: User['id']) {
        return this.with({ id });
    }

    withEmail(email: User['email']) {
        return this.with({ email });
    }

    withName(name: User['name']) {
        return this.with({ name });
    }

    withIsActive(isActive: User['isActive']) {
        return this.with({ isActive });
    }
}

3. Use in your tests:

import { UserBuilder } from '...';

const testUser = new UserBuilder()
  .withEmail('test@example.com')
  .withIsActive(false)
  .build();

Why?

Tests often become cluttered with boilerplate when you need to create complex objects just to test one specific field. DataBuilders let you focus on what matters: Imagine testing a case where document aggregate should emit an event when it successfully update it's content:

it('should emit a ContentUpdatedEvent', () => {
  const aggregate = DocumentAggregate.create({
    id: '1',
    createdAt: new Date(),
    updatedAt: new Date(),
    content: 'old-content'
  });
  const userId = '1';

  aggregate.updateContent({ updatedBy: userId, content: 'new-content' });

  expect(...);
})

Above code is obfuscated with all of the default values you need to provide in order to satisfy typescript. Where in reality the only thing specific to this single test is the fact that some new content was provided to updateContent method.

Imagine even more complex scenario:

it('should show validation error when email is invalid', async () => {
  render(<ProfileForm defaultValues={{
      firstName: '',
      lastName: '',
      age: 0,
      socials: {
        linkedin: '',
        github: '',
        website: '',
        twitter: '',
      },
      address: {
        street: '',
        city: '',
        state: '',
        zip: '',
      },
      skills: [],
      bio: '',
      email: 'invalid-email'
    }}
  />)

  await submitForm();

  expect(...);
})

Again - in reality you should only be worried about email, not about whole form data.

Here's how above tests could be written with databuilders:

it('should emit a ContentUpdatedEvent', () => {
  const aggregate = DocumentAggregate.create(
    new CreateDocumentAggregatedPayloadBuilder().build()
  );

  aggregate.updateContent(
    new UpdateDocumentContentPayloadBuilder().withContent('new-content').build()
  );

  expect(...);
})
it('should show validation error when email is invalid', async () => {
  render(<ProfileForm defaultValues={
    new ProfileFormInputBuilder.withEmail('invalid-email').build()} />
  )

  await submitForm();

  expect(...);
})

This not only makes the test code less verbose but also highlights what is really being tested.

Why not use AI for that? While AI can generate test data, ts-databuilders is fast, free and deterministic.

Read more about data builders.

Nested Builders

Note

Nested builders can be turned off by using withNestedBuilders option. Check configuration section for more details.

When your types contain complex nested objects, you can annotate their type definitions and TS DataBuilders will automatically generate nested builders, allowing you to compose them fluently.

Example

Input types:

/**
 * @DataBuilder
 */
export type User = {
  name: string;
  address: Address;
};

/**
 * @DataBuilder
 */
export type Address = {
  street: string;
  city: string;
  country: string;
};

Generated builders:

export class UserBuilder extends DataBuilder<User> {
    constructor() {
        super({
          name: "",
          address: new AddressBuilder().build();
        });
    }

    withName(name: User['name']) {
        return this.with({ name });
    }

    withAddress(address: DataBuilder<User['address']>) {
        return this.with({ address: address.build() });
    }
}

export class AddressBuilder extends DataBuilder<Address> {
    constructor() {
        super({
          street: "",
          city: "",
          country: ""
        });
    }

    withStreet(street: Address['street']) {
        return this.with({ street });
    }

    withCity(city: Address['city']) {
        return this.with({ city });
    }

    withCountry(country: Address['country']) {
        return this.with({ country });
    }
}

Usage:

// ✅ Compose builders fluently
const user = new UserBuilder()
  .withName('John Doe')
  .withAddress(
    new AddressBuilder()
      .withStreet('123 Main St')
      .withCity('New York')
  )
  .build();
// {..., address: { street: "123 Main st", city: "New York", country: "" } }

// ✅ Use default values
const userWithDefaultAddress = new UserBuilder().build();
// {..., address: { street: "", city: "", country: "" } }

// ✅ Override just one nested field
const userWithCity = new UserBuilder()
  .withAddress(
    new AddressBuilder()
      .withCity('San Francisco')
  )
  .build();
// {..., address: { street: "", city: "San Francisco", country: "" } }

Inline Default Values

Note

It's your responsibility to provide inline default value that satisfies expected type.

While global defaults work well for most cases, sometimes you need field-specific default values. This is especially important for specialized string types like ISO dates, UUIDs etc.

/** @DataBuilder */
type Order = {
  id: string;           // Empty string - won't work as UUID
  createdAt: string;    // Empty string - Invalid Date!
}

// Generated:
constructor() {
  super({
    id: "",
    createdAt: "",  // new Date("") = Invalid Date
  });
}

Use @DataBuilderDefault JSDoc tag to override defaults per field:

/** @DataBuilder */
type Order = {
  /** @DataBuilderDefault '550e8400-e29b-41d4-a716-446655440000' */
  id: string;

  /** @DataBuilderDefault '2025-11-05T15:32:58.727Z' */
  createdAt: string;
}

// Generated:
constructor() {
  super({
    id: '550e8400-e29b-41d4-a716-446655440000',
    createdAt: '2025-11-05T15:32:58.727Z',
  });
}

Supported Types

The library supports a wide range of TypeScript type features:

Primitives & Built-ins

  • string, number, boolean, Date
  • Literal types: 'active' | 'inactive', 1 | 2 | 3

Complex Structures

  • Objects and nested objects
  • Arrays: string[], Array<number>
  • Tuples: [string, number]
  • Records: Record<string, string> Record<'foo' | 'bar', string>

Type Operations

  • Unions: string | number | true | false
  • Intersections: A & B
  • Utility types: Pick<T, K>, Omit<T, K>, Partial<T>, Required<T>, Readonly<T>, Extract<T, U>, NonNullable<T>
  • Branded types: type UserId = string & { __brand: 'UserId' }

References

  • Type references from the same file
  • Type references from other files
  • External library types (e.g., z.infer<typeof schema>)

For a comprehensive example of supported types, check out the example-data/bar.ts file in the repository. This file is used during development and demonstrates complex real-world type scenarios.

Important Rules & Limitations

Unique Builder Names

Each type annotated with the JSDoc tag must have a unique name across your codebase:

// ❌ Error: Duplicate builder names
// In file-a.ts
/** @DataBuilder */
export type User = { name: string };

// In file-b.ts
/** @DataBuilder */
export type User = { email: string };  // 💥 Duplicate!

Exported Types Only

Types must be exported to generate builders:

// ❌ Won't work
/** @DataBuilder */
type User = { name: string };

// ✅ Works
/** @DataBuilder */
export type User = { name: string };

Type Aliases Only

Currently, only type aliases are supported as root builder types. Interfaces, classes etc. are not supported:

// ❌ Not supported
/** @DataBuilder */
export interface User {
  name: string;
}

// ❌ Not supported
/** @DataBuilder */
export class User {
  name: string;
}

// ✅ Supported
/** @DataBuilder */
export type User = {
  name: string;
};

Unsupported TypeScript Features

Some TypeScript features are not yet supported and will cause generation errors:

  • Recursive types: Types that reference themselves
  // ❌ Not supported
  type TreeNode = {
    value: string;
    children: TreeNode[];  // Self-reference
  };
  • Function types: Properties that are functions
  // ❌ Not supported
  type WithCallback = {
    onSave: (data: string) => void;
  };
  • typeof, keyof, unknown

Alpha Stage

⚠️ This library is in active development

  • Breaking changes may occur
  • Not all edge cases are covered yet
  • Test thoroughly before using in production

Found an issue? Please report it on GitHub with:

  • A minimal reproducible example (if possible)
  • The type definition causing the issue
  • The error message received
  • Your ts-databuilders.json config and any provided CLI flags (if applicable)

You can also turn on debug logs by passing --log-level debug flag.

Your feedback helps improve the library for everyone! 🙏

Similar Projects

  • effect-builder - a runtime library for building objects with Effect Schema validation.

Contributing

Contributions welcome! Please open an issue or PR on GitHub.

License

MIT © nemmtor

About

Generator for TypeScript DataBuilders

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

  •  

Contributors