
Product
Introducing Repository Access Permissions and Custom Roles
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.
@cerios/cerios-builder
Advanced tools
A TypeScript builder pattern library providing compile-time type safety for object construction with method chaining and required field validation
A powerful TypeScript builder pattern library that provides compile-time type safety for object construction. Build complex objects with method chaining while ensuring all required properties are set at compile time.
npm install @cerios/cerios-builder
dev install
npm install --save-dev @cerios/cerios-builder
import { CeriosBuilder, RequiredFieldsTemplate } from "@cerios/cerios-builder";
// 1. Define your types
type User = {
id: string;
name: string;
email: string;
age?: number;
};
// 2. Create your builder
class UserBuilder extends CeriosBuilder<User> {
static create() {
return new UserBuilder({});
}
id(value: string) {
return this.setProperty("id", value);
}
name(value: string) {
return this.setProperty("name", value);
}
email(value: string) {
return this.setProperty("email", value);
}
age(value: number) {
return this.setProperty("age", value);
}
}
// 3. Build your object
const user = UserBuilder.create().id("123").name("John Doe").email("john@example.com").age(30).build();
| Feature | Method | Use Case |
|---|---|---|
| Simple Properties | setProperty() | Set flat object properties |
| Nested Properties | setNestedProperty() | Set deeply nested properties with dot notation |
| Array Properties | addToArrayProperty() | Add values to array properties |
| Multiple Properties | setProperties() | Set multiple properties at once |
| Required Template | static requiredTemplate | Define required fields for runtime validation |
| Dynamic Requirements | setRequiredFields() | Add required fields at runtime |
| Custom Validators | addValidator() | Add custom validation logic with error messages |
| Property Removal | removeOptionalProperty() | Remove optional properties from builder |
| Clear Optional Props | clearOptionalProperties() | Clear all optional properties, keep required |
| Full Validation | build() | Build with both compile-time and runtime validation |
| Compile-Time Only | buildWithoutRuntimeValidation() | Build with TypeScript checking only |
| Runtime Only | buildWithoutCompileTimeValidation() | Build with runtime validation only |
| No Validation | buildUnsafe() | Build without any validation |
| Partial Build | buildPartial() | Build incomplete objects |
| Shallow Freeze | buildFrozen() | Create immutable object (top-level only) |
| Deep Freeze | buildDeepFrozen() | Create fully immutable object tree |
| Shallow Seal | buildSealed() | Lock structure, allow modifications |
| Deep Seal | buildDeepSealed() | Lock all structures, allow modifications |
import { CeriosBuilder } from "@cerios/cerios-builder";
// Define your data type
type User = {
id: string;
name: string;
email: string;
age?: number; // Optional property
roles?: string[]; // Optional array property
};
// Create your builder class
class UserBuilder extends CeriosBuilder<User> {
static create() {
return new UserBuilder({});
}
id(value: string) {
return this.setProperty("id", value);
}
name(value: string) {
return this.setProperty("name", value);
}
email(value: string) {
return this.setProperty("email", value);
}
age(value: number) {
return this.setProperty("age", value);
}
addRole(role: string) {
return this.addToArrayProperty("roles", role);
}
// Custom methods for common patterns
withRandomId() {
return this.setProperty("id", crypto.randomUUID());
}
withAdminEmail(username: string) {
return this.setProperty("email", `${username}@admin.company.com`);
}
}
// ✅ This compiles - all required fields are set
const user = UserBuilder.create()
.id("123")
.name("John Doe")
.email("john@example.com")
.age(30)
.addRole("admin")
.addRole("editor")
.build();
// ✅ Optional fields and arrays can be omitted
const basicUser = UserBuilder.create().id("456").name("Jane Doe").email("jane@example.com").build(); // age and roles are optional
// ❌ This won't compile - missing required 'email' field
const incompleteUser = UserBuilder.create().id("789").name("Bob Smith").build(); // 💥 TypeScript error!
If you want explicit return types on custom fluent methods, use BuilderStep and ClassBuilderStep.
These helpers keep method signatures readable while preserving compile-time required-field tracking.
import { BuilderStep, CeriosBuilder } from "@cerios/cerios-builder";
type User = {
name: string;
age: number;
address: {
city: string;
};
};
class UserBuilder extends CeriosBuilder<User> {
static create() {
return new UserBuilder({});
}
name(value: string): BuilderStep<this, User, "name"> {
return this.setProperty("name", value);
}
age(value: number): BuilderStep<this, User, "age"> {
return this.setProperty("age", value);
}
city(value: string): BuilderStep<this, User, "address.city"> {
return this.setNestedProperty("address.city", value);
}
}
import { CeriosClassBuilder, ClassBuilderStep, ClassConstructor } from "@cerios/cerios-builder";
class Person {
name!: string;
address!: { city: string };
}
class PersonBuilder extends CeriosClassBuilder<Person> {
constructor(classConstructor: ClassConstructor<Person> = Person, data: Partial<Person> = {}) {
super(classConstructor, data);
}
static create() {
return new PersonBuilder(Person);
}
name(value: string): ClassBuilderStep<this, Person, "name"> {
return this.setProperty("name", value);
}
city(value: string): ClassBuilderStep<this, Person, "address.city"> {
return this.setNestedProperty("address.city", value);
}
}
const adminUser = UserBuilder.create()
.withRandomId()
.name("Admin User")
.withAdminEmail("admin")
.addRole("admin")
.age(25)
.build();
console.log(adminUser);
// Output: { id: "550e8400-...", name: "Admin User", email: "admin@admin.company.com", roles: ["admin"], age: 25 }
Build complex nested structures with type-safe dot-notation paths:
import { CeriosBuilder, RequiredFieldsTemplate } from "@cerios/cerios-builder";
type Address = {
Street: string;
City: string;
PostalCode?: string;
Country: string;
};
type OrderDetails = {
OrderNumber?: string;
CustomerId: string;
TotalAmount: number;
Status: string;
ShippingAddress: Address;
};
type Order = {
Details?: OrderDetails;
};
type OrderRequest = {
Order: Order;
};
class OrderRequestBuilder extends CeriosBuilder<OrderRequest> {
static requiredTemplate: RequiredFieldsTemplate<OrderRequest> = [
"Order.Details.CustomerId",
"Order.Details.TotalAmount",
"Order.Details.Status",
"Order.Details.ShippingAddress.Street",
"Order.Details.ShippingAddress.City",
"Order.Details.ShippingAddress.Country",
];
static create() {
return new OrderRequestBuilder({});
}
static createWithDefaults() {
return this.create().status("pending");
}
orderNumber(value: string) {
return this.setNestedProperty("Order.Details.OrderNumber", value);
}
customerId(value: string) {
return this.setNestedProperty("Order.Details.CustomerId", value);
}
totalAmount(value: number) {
return this.setNestedProperty("Order.Details.TotalAmount", value);
}
status(value: string) {
return this.setNestedProperty("Order.Details.Status", value);
}
shippingStreet(value: string) {
return this.setNestedProperty("Order.Details.ShippingAddress.Street", value);
}
shippingCity(value: string) {
return this.setNestedProperty("Order.Details.ShippingAddress.City", value);
}
shippingPostalCode(value: string) {
return this.setNestedProperty("Order.Details.ShippingAddress.PostalCode", value);
}
shippingCountry(value: string) {
return this.setNestedProperty("Order.Details.ShippingAddress.Country", value);
}
}
// Usage with full validation (both compile-time and runtime)
const order = OrderRequestBuilder.createWithDefaults()
.customerId("CUST-001")
.totalAmount(299.99)
.orderNumber("ORD-12345")
.shippingStreet("123 Main St")
.shippingCity("New York")
.shippingCountry("USA")
.build(); // ✅ TypeScript + runtime validation
// ❌ This will throw an error at runtime
try {
const invalidOrder = OrderRequestBuilder.createWithDefaults()
.customerId("CUST-002")
.buildWithoutCompileTimeValidation(); // Missing TotalAmount and shipping address
} catch (error) {
console.error(error.message);
// "Missing required fields: Order.Details.TotalAmount, Order.Details.ShippingAddress.Street, ..."
}
Define which fields are required and validate them at runtime:
type Product = {
id: string;
name: string;
price: number;
description?: string;
};
class ProductBuilder extends CeriosBuilder<Product> {
static requiredTemplate: RequiredFieldsTemplate<Product> = ["id", "name", "price"];
static create() {
return new ProductBuilder({});
}
id(value: string) {
return this.setProperty("id", value);
}
name(value: string) {
return this.setProperty("name", value);
}
price(value: number) {
return this.setProperty("price", value);
}
description(value: string) {
return this.setProperty("description", value);
}
}
// Full validation (compile-time + runtime)
const product = ProductBuilder.create().id("PROD-001").name("Laptop").price(999.99).build(); // ✅ All required fields are set
// Runtime-only validation (useful for dynamic data)
const dynamicProduct = ProductBuilder.create()
.id("PROD-002")
.name("Mouse")
.price(29.99)
.buildWithoutCompileTimeValidation(); // ✅ Runtime check passes
// ❌ This will throw an error at runtime
try {
const invalidProduct = ProductBuilder.create().id("PROD-003").name("Keyboard").buildWithoutCompileTimeValidation(); // Missing price
} catch (error) {
console.error(error.message);
// "Missing required fields: price"
}
Add required fields at runtime based on business logic:
type Employee = {
firstName: string;
lastName: string;
email: string;
employeeId?: string;
phone?: string;
};
class EmployeeBuilder extends CeriosBuilder<Employee> {
static requiredTemplate: RequiredFieldsTemplate<Employee> = ["firstName", "lastName", "email"];
static create() {
return new EmployeeBuilder({});
}
firstName(value: string) {
return this.setProperty("firstName", value);
}
lastName(value: string) {
return this.setProperty("lastName", value);
}
email(value: string) {
return this.setProperty("email", value);
}
employeeId(value: string) {
return this.setProperty("employeeId", value);
}
phone(value: string) {
return this.setProperty("phone", value);
}
}
// Scenario 1: New employee (only basic fields required)
const newEmployee = EmployeeBuilder.create().firstName("John").lastName("Doe").email("john.doe@company.com").build(); // ✅ Only validates firstName, lastName, email
// Scenario 2: Existing employee (ID required)
const existingEmployee = EmployeeBuilder.create()
.setRequiredFields(["employeeId"]) // Add employeeId as required dynamically
.firstName("Jane")
.lastName("Smith")
.email("jane.smith@company.com")
.employeeId("EMP-12345")
.buildWithoutCompileTimeValidation(); // ✅ Runtime validates all fields including employeeId
// Scenario 3: Employee with contact requirement
const contactEmployee = EmployeeBuilder.create()
.setRequiredFields(["phone", "employeeId"]) // Add multiple dynamic fields
.firstName("Bob")
.lastName("Johnson")
.email("bob.johnson@company.com")
.phone("+1-555-0123")
.employeeId("EMP-67890")
.buildWithoutCompileTimeValidation(); // ✅ Validates all including phone and employeeId
// ❌ This will fail - missing dynamically required field
try {
const invalidEmployee = EmployeeBuilder.create()
.setRequiredFields(["employeeId"])
.firstName("Alice")
.lastName("Brown")
.email("alice.brown@company.com")
.buildWithoutCompileTimeValidation(); // Missing employeeId
} catch (error) {
console.error(error.message);
// "Missing required fields: employeeId"
}
When you have related request types, share fluent methods in a generic base builder and keep concrete builders per request type. This preserves type-safe chaining and allows each request type to keep its own required-field template.
import { BuilderStep, CeriosBuilder, RequiredFieldsTemplate } from "@cerios/cerios-builder";
type BasePostRequest = {
postId: string;
title?: string;
content?: string;
tags?: string[];
};
type CreatePostRequest = BasePostRequest & {
title: string;
content: string;
authorId: string;
};
type PatchPostRequest = BasePostRequest & {
patchReason: string;
notifySubscribers?: boolean;
};
abstract class BasePostRequestBuilder<T extends BasePostRequest> extends CeriosBuilder<T> {
postId(value: string): BuilderStep<this, T, "postId"> {
return this.setProperty("postId", value as T["postId"]);
}
title(value: string): BuilderStep<this, T, "title"> {
return this.setProperty("title", value as T["title"]);
}
content(value: string): BuilderStep<this, T, "content"> {
return this.setProperty("content", value as T["content"]);
}
addTag(value: string): BuilderStep<this, T, "tags"> {
return this.addToArrayProperty("tags", value);
}
}
class CreatePostRequestBuilder extends BasePostRequestBuilder<CreatePostRequest> {
static create() {
return new CreatePostRequestBuilder({});
}
authorId(value: string): BuilderStep<this, CreatePostRequest, "authorId"> {
return this.setProperty("authorId", value);
}
}
class PatchPostRequestBuilder extends BasePostRequestBuilder<PatchPostRequest> {
static create() {
return new PatchPostRequestBuilder({});
}
patchReason(value: string): BuilderStep<this, PatchPostRequest, "patchReason"> {
return this.setProperty("patchReason", value);
}
notifySubscribers(value: boolean): BuilderStep<this, PatchPostRequest, "notifySubscribers"> {
return this.setProperty("notifySubscribers", value);
}
}
const createRequest = CreatePostRequestBuilder.create()
.postId("post-100")
.title("Builder patterns")
.content("Full create payload")
.authorId("author-1")
.addTag("typescript")
.build();
const patchRequest = PatchPostRequestBuilder.create()
.postId("post-100")
.patchReason("Fix title")
.title("Updated title")
.notifySubscribers(true)
.build();
The same pattern works with CeriosClassBuilder.
See the class-based test example in tests/cerios-class-builder/cerios-class-builder-post-patch-extension.test.ts.
When working with deeply nested structures, you have multiple approaches to configure required field validation:
type Address = {
Street: string;
City: string;
Country: string;
};
type OrderDetails = {
CustomerId: string;
TotalAmount: number;
ShippingAddress: Address;
};
type Order = {
Details: OrderDetails;
};
class OrderBuilder extends CeriosBuilder<Order> {
// Method 1: Static template defined at class level
static requiredTemplate: RequiredFieldsTemplate<Order> = [
"Details.CustomerId",
"Details.TotalAmount",
"Details.ShippingAddress.Street",
"Details.ShippingAddress.City",
"Details.ShippingAddress.Country",
];
static create() {
return new OrderBuilder({});
}
customerId(value: string) {
return this.setNestedProperty("Details.CustomerId", value);
}
totalAmount(value: number) {
return this.setNestedProperty("Details.TotalAmount", value);
}
shippingStreet(value: string) {
return this.setNestedProperty("Details.ShippingAddress.Street", value);
}
shippingCity(value: string) {
return this.setNestedProperty("Details.ShippingAddress.City", value);
}
shippingCountry(value: string) {
return this.setNestedProperty("Details.ShippingAddress.Country", value);
}
}
// Usage: Runtime validation checks all required fields
const order = OrderBuilder.create()
.customerId("CUST-001")
.totalAmount(299.99)
.shippingStreet("123 Main St")
.shippingCity("New York")
.shippingCountry("USA")
.buildWithoutCompileTimeValidation(); // ✅ Validates all 5 required nested fields
setRequiredFields() (Best for Conditional Logic)class OrderBuilder extends CeriosBuilder<Order> {
static requiredTemplate: RequiredFieldsTemplate<Order> = ["Details.CustomerId", "Details.TotalAmount"];
static create() {
return new OrderBuilder({});
}
static createInternational() {
return this.create().setRequiredFields([
"Details.ShippingAddress.Street",
"Details.ShippingAddress.City",
"Details.ShippingAddress.Country",
"Details.ShippingAddress.PostalCode", // Extra requirement for international
]);
}
static createDomestic() {
return this.create().setRequiredFields([
"Details.ShippingAddress.Street",
"Details.ShippingAddress.City",
"Details.ShippingAddress.Country",
]); // No postal code required
}
customerId(value: string) {
return this.setNestedProperty("Details.CustomerId", value);
}
totalAmount(value: number) {
return this.setNestedProperty("Details.TotalAmount", value);
}
shippingStreet(value: string) {
return this.setNestedProperty("Details.ShippingAddress.Street", value);
}
shippingCity(value: string) {
return this.setNestedProperty("Details.ShippingAddress.City", value);
}
shippingCountry(value: string) {
return this.setNestedProperty("Details.ShippingAddress.Country", value);
}
shippingPostalCode(value: string) {
return this.setNestedProperty("Details.ShippingAddress.PostalCode", value);
}
}
// Domestic order: postal code is optional
const domesticOrder = OrderBuilder.createDomestic()
.customerId("CUST-001")
.totalAmount(99.99)
.shippingStreet("123 Main St")
.shippingCity("New York")
.shippingCountry("USA")
.buildWithoutCompileTimeValidation(); // ✅ Valid without postal code
// International order: postal code is required
const internationalOrder = OrderBuilder.createInternational()
.customerId("CUST-002")
.totalAmount(299.99)
.shippingStreet("10 Downing Street")
.shippingCity("London")
.shippingCountry("UK")
.shippingPostalCode("SW1A 2AA")
.buildWithoutCompileTimeValidation(); // ✅ All fields including postal code validated
// ❌ This will fail - missing postal code for international
try {
const invalidOrder = OrderBuilder.createInternational()
.customerId("CUST-003")
.totalAmount(150.0)
.shippingStreet("Rue de Rivoli")
.shippingCity("Paris")
.shippingCountry("France")
.buildWithoutCompileTimeValidation(); // Missing PostalCode
} catch (error) {
console.error(error.message);
// "Missing required fields: Details.ShippingAddress.PostalCode"
}
class OrderBuilder extends CeriosBuilder<Order> {
// Base template with common required fields
static requiredTemplate: RequiredFieldsTemplate<Order> = ["Details.CustomerId", "Details.TotalAmount"];
// Constructor accepts additional required fields
constructor(initial: Partial<Order>, additionalRequiredFields?: RequiredFieldsTemplate<Order>) {
super(initial, additionalRequiredFields);
}
static create() {
return new OrderBuilder({});
}
// Factory method for orders with shipping requirements
static createWithShipping() {
return new OrderBuilder({}, [
"Details.ShippingAddress.Street",
"Details.ShippingAddress.City",
"Details.ShippingAddress.Country",
]);
}
customerId(value: string) {
return this.setNestedProperty("Details.CustomerId", value);
}
totalAmount(value: number) {
return this.setNestedProperty("Details.TotalAmount", value);
}
shippingStreet(value: string) {
return this.setNestedProperty("Details.ShippingAddress.Street", value);
}
shippingCity(value: string) {
return this.setNestedProperty("Details.ShippingAddress.City", value);
}
shippingCountry(value: string) {
return this.setNestedProperty("Details.ShippingAddress.Country", value);
}
}
// Order without shipping (only validates base required fields)
const digitalOrder = OrderBuilder.create()
.customerId("CUST-001")
.totalAmount(49.99)
.buildWithoutCompileTimeValidation(); // ✅ Valid - shipping not required
// Order with shipping (validates base + shipping fields)
const physicalOrder = OrderBuilder.createWithShipping()
.customerId("CUST-002")
.totalAmount(99.99)
.shippingStreet("123 Main St")
.shippingCity("New York")
.shippingCountry("USA")
.buildWithoutCompileTimeValidation(); // ✅ All required fields validated
// ❌ This will fail - missing shipping address
try {
const invalidOrder = OrderBuilder.createWithShipping()
.customerId("CUST-003")
.totalAmount(75.0)
.buildWithoutCompileTimeValidation(); // Missing shipping fields
} catch (error) {
console.error(error.message);
// "Missing required fields: Details.ShippingAddress.Street, ..."
}
class OrderBuilder extends CeriosBuilder<Order> {
// Static template for always-required fields
static requiredTemplate: RequiredFieldsTemplate<Order> = ["Details.CustomerId", "Details.TotalAmount"];
static create() {
return new OrderBuilder({});
}
// Factory with static template + additional dynamic fields
static createPriorityOrder() {
return this.create().setRequiredFields([
"Details.ShippingAddress.Street",
"Details.ShippingAddress.City",
"Details.ShippingAddress.Country",
"Details.ShippingAddress.PostalCode",
"Details.Priority", // Extra field for priority orders
]);
}
// Another factory combining different requirements
static createGiftOrder() {
return this.create().setRequiredFields([
"Details.ShippingAddress.Street",
"Details.ShippingAddress.City",
"Details.ShippingAddress.Country",
"Details.GiftMessage", // Required for gift orders
"Details.RecipientName", // Required for gift orders
]);
}
customerId(value: string) {
return this.setNestedProperty("Details.CustomerId", value);
}
totalAmount(value: number) {
return this.setNestedProperty("Details.TotalAmount", value);
}
priority(value: string) {
return this.setNestedProperty("Details.Priority", value);
}
giftMessage(value: string) {
return this.setNestedProperty("Details.GiftMessage", value);
}
recipientName(value: string) {
return this.setNestedProperty("Details.RecipientName", value);
}
shippingStreet(value: string) {
return this.setNestedProperty("Details.ShippingAddress.Street", value);
}
shippingCity(value: string) {
return this.setNestedProperty("Details.ShippingAddress.City", value);
}
shippingCountry(value: string) {
return this.setNestedProperty("Details.ShippingAddress.Country", value);
}
shippingPostalCode(value: string) {
return this.setNestedProperty("Details.ShippingAddress.PostalCode", value);
}
}
// Priority order: requires shipping + priority + base fields
const priorityOrder = OrderBuilder.createPriorityOrder()
.customerId("CUST-001")
.totalAmount(299.99)
.priority("express")
.shippingStreet("123 Main St")
.shippingCity("New York")
.shippingCountry("USA")
.shippingPostalCode("10001")
.buildWithoutCompileTimeValidation(); // ✅ All priority order fields validated
// Gift order: requires shipping + gift fields + base fields
const giftOrder = OrderBuilder.createGiftOrder()
.customerId("CUST-002")
.totalAmount(149.99)
.giftMessage("Happy Birthday!")
.recipientName("John Doe")
.shippingStreet("456 Oak Ave")
.shippingCity("Boston")
.shippingCountry("USA")
.buildWithoutCompileTimeValidation(); // ✅ All gift order fields validated
// ❌ This will fail - missing gift-specific fields
try {
const invalidGiftOrder = OrderBuilder.createGiftOrder()
.customerId("CUST-003")
.totalAmount(99.99)
.shippingStreet("789 Elm St")
.shippingCity("Chicago")
.shippingCountry("USA")
.buildWithoutCompileTimeValidation(); // Missing GiftMessage and RecipientName
} catch (error) {
console.error(error.message);
// "Missing required fields: Details.GiftMessage, Details.RecipientName"
}
Summary of the Four Approaches:
| Approach | When to Use | Flexibility | Complexity |
|---|---|---|---|
| 1. Static Template | Fixed requirements that never change | Low | Low |
2. Dynamic via setRequiredFields() | Requirements change based on runtime conditions | High | Medium |
| 3. Constructor Parameter | Requirements known at builder creation time | Medium | Medium |
| 4. Hybrid (Static + Dynamic) | Common base requirements + context-specific additions | High | Higher |
Perfect for creating test fixtures with sensible defaults:
type Product = {
name: string;
price: number;
category: string;
tags?: string[];
};
class ProductBuilder extends CeriosBuilder<Product> {
static create() {
return new ProductBuilder({});
}
name(value: string) {
return this.setProperty("name", value);
}
price(value: number) {
return this.setProperty("price", value);
}
category(value: string) {
return this.setProperty("category", value);
}
addTag(tag: string) {
return this.addToArrayProperty("tags", tag);
}
// Custom methods for testing
asElectronics() {
return this.category("Electronics").price(299.99);
}
asFreeProduct() {
return this.price(0);
}
withRandomName() {
const names = ["Widget", "Gadget", "Tool", "Device"];
return this.name(names[Math.floor(Math.random() * names.length)]);
}
}
// In your tests
const testProduct = ProductBuilder.create().withRandomName().asElectronics().addTag("featured").addTag("sale").build();
type Address = {
street: string;
city: string;
country: string;
zipCode?: string;
};
type Customer = {
id: string;
name: string;
address: Address;
phoneNumber?: string;
notes?: string[];
};
class AddressBuilder extends CeriosBuilder<Address> {
static create() {
return new AddressBuilder({});
}
static createWithDefaults() {
return this.create().setProperties({
city: "Othertown",
country: "United States",
});
}
// NEW: Create with ALL required fields set
static createComplete() {
return this.create().street("123 Default Street").city("Othertown").country("United States");
}
street(value: string) {
return this.setProperty("street", value);
}
city(value: string) {
return this.setProperty("city", value);
}
country(value: string) {
return this.setProperty("country", value);
}
zipCode(value: string) {
return this.setProperty("zipCode", value);
}
asUSAddress() {
return this.country("United States");
}
}
class CustomerBuilder extends CeriosBuilder<Customer> {
static create() {
return new CustomerBuilder({});
}
id(value: string) {
return this.setProperty("id", value);
}
name(value: string) {
return this.setProperty("name", value);
}
address(value: Address) {
return this.setProperty("address", value);
}
phoneNumber(value: string) {
return this.setProperty("phoneNumber", value);
}
addNote(note: string) {
return this.addToArrayProperty("notes", note);
}
// Pattern 1: No defaults - must set all required fields
withAddress(builderFn: BuilderComposerFromFactory<typeof AddressBuilder.create>) {
const address = builderFn(AddressBuilder.create()).build();
return this.setProperty("address", address);
}
// Pattern 2: Partial defaults - must set remaining required fields (street)
withAddressDefaults(builderFn: BuilderComposerFromFactory<typeof AddressBuilder.createWithDefaults>) {
const address = builderFn(AddressBuilder.createWithDefaults()).build();
return this.setProperty("address", address);
}
// Pattern 3: Complete defaults - callback is OPTIONAL
withCompleteAddress(builderFn?: BuilderComposerFromFactory<typeof AddressBuilder.createComplete>) {
const builder = AddressBuilder.createComplete();
const address = builderFn ? builderFn(builder).build() : builder.build();
return this.setProperty("address", address);
}
}
BuilderComposerFromFactory<> for Simpler Type SignaturesWhen accepting builders with defaults already set, use BuilderComposerFromFactory<> to infer callback input/output types from the factory automatically.
import { BuilderComposerFromFactory, CeriosBuilder } from "@cerios/cerios-builder";
class CustomerBuilder extends CeriosBuilder<Customer> {
// No manual Pick<> or ReturnType<> needed:
withAddressDefaults(builderFn: BuilderComposerFromFactory<typeof AddressBuilder.createWithDefaults>) {
const address = builderFn(AddressBuilder.createWithDefaults()).build();
return this.setProperty("address", address);
}
}
Benefits:
Pick<>createWithDefaults() automatically update the typeUsage examples:
// Pattern 1: No defaults - must set all required fields
const customer1 = CustomerBuilder.create()
.id("CUST-001")
.name("Alice Johnson")
.withAddress((addr) => addr.street("123 Main St").city("New York").asUSAddress().zipCode("10001"))
.addNote("VIP customer")
.phoneNumber("+1-555-0123")
.build();
// Pattern 2: Partial defaults - city and country already set, must provide street
const customer2 = CustomerBuilder.create()
.id("CUST-002")
.name("Bob Smith")
.withAddressDefaults((addr) => addr.street("456 Elm St"))
.addNote("New customer")
.build();
console.log(customer2);
// Output: { id: 'CUST-002', name: 'Bob Smith', address: { street: '456 Elm St', city: 'Othertown', country: 'United States' }, notes: ['New customer'] }
// Pattern 3a: Complete defaults - no callback needed, use all defaults
const customer3 = CustomerBuilder.create()
.id("CUST-003")
.name("Charlie Davis")
.withCompleteAddress() // ✅ No callback needed - all required fields already set!
.build();
console.log(customer3);
// Output: { id: 'CUST-003', name: 'Charlie Davis', address: { street: '123 Default Street', city: 'Othertown', country: 'United States' } }
// Pattern 3b: Complete defaults - optional callback to modify/add optional fields
const customer4 = CustomerBuilder.create()
.id("CUST-004")
.name("Diana Evans")
.withCompleteAddress(
(addr) => addr.zipCode("90210"), // Only modify/add optional fields if needed
)
.build();
console.log(customer4);
// Output: { id: 'CUST-004', name: 'Diana Evans', address: { street: '123 Default Street', city: 'Othertown', country: 'United States', zipCode: '90210' } }
// Pattern 3c: Complete defaults - optional callback to override any field
const customer5 = CustomerBuilder.create()
.id("CUST-005")
.name("Eve Foster")
.withCompleteAddress((addr) =>
addr
.street("789 Custom Ave") // Override the default street
.zipCode("10001"),
)
.addNote("Premium customer")
.build();
console.log(customer5);
// Output: { id: 'CUST-005', name: 'Eve Foster', address: { street: '789 Custom Ave', city: 'Othertown', country: 'United States', zipCode: '10001' }, notes: ['Premium customer'] }
createWithDefaults() with Nested BuildersThe key pattern is:
createComplete(): Returns a builder with all required fields already setoptional (?), so you don't have to provide itbuild() immediatelyThis pattern is perfect when:
Comparison of the three patterns:
// Pattern 1: Must set ALL required fields
.withAddress(addr => addr.street('...').city('...').country('...'))
// Pattern 2: Must set REMAINING required fields (street)
.withAddressDefaults(addr => addr.street('...'))
// Pattern 3a: NO callback needed - all defaults
.withCompleteAddress()
// Pattern 3b: OPTIONAL callback - only if you want to customize
.withCompleteAddress(addr => addr.zipCode('...'))
// Pattern 3c: OPTIONAL callback - can override any field
.withCompleteAddress(addr => addr.street('...').zipCode('...'))
Cerios Builder supports custom validation logic that runs when you build an object. Validators give you fine-grained control over business rules and constraints beyond simple "field required" checks.
Use the addValidator() method to add custom validation logic. Validators receive the partial object being built and can:
true if validation passesfalse if validation fails (generic error)string error message for detailed feedbacktype User = {
name: string;
email: string;
age?: number;
password?: string;
confirmPassword?: string;
};
class UserBuilder extends CeriosBuilder<User> {
static requiredTemplate: RequiredFieldsTemplate<User> = ["name", "email"];
static create() {
return new UserBuilder({});
}
name(value: string) {
return this.setProperty("name", value);
}
email(value: string) {
return this.setProperty("email", value);
}
age(value: number) {
return this.setProperty("age", value);
}
password(value: string) {
return this.setProperty("password", value);
}
confirmPassword(value: string) {
return this.setProperty("confirmPassword", value);
}
}
// Example 1: Simple boolean validation
const user1 = UserBuilder.create()
.name("John Doe")
.email("john@example.com")
.age(20)
.addValidator((obj) => (obj.age ?? 0) >= 18) // Returns true/false
.build(); // ✅ Passes - age is >= 18
// Example 2: Validation with custom error messages
try {
const user2 = UserBuilder.create()
.name("Jane Smith")
.email("invalid-email")
.addValidator((obj) => (obj.email?.includes("@") ? true : "Email must contain @"))
.build();
} catch (error) {
console.error(error.message);
// Output: "Validation failed: Email must contain @"
}
// Example 3: Multiple validators
const user3 = UserBuilder.create()
.name("Bob Johnson")
.email("bob@example.com")
.age(25)
.password("secret123")
.confirmPassword("secret123")
.addValidator((obj) => (obj.age ?? 0) >= 18 || "User must be at least 18 years old")
.addValidator((obj) => obj.email?.includes("@") || "Invalid email format")
.addValidator((obj) => {
if (obj.password && obj.confirmPassword) {
return obj.password === obj.confirmPassword || "Passwords do not match";
}
return true; // Skip validation if passwords not set
})
.build(); // ✅ All validators pass
| Return Type | Meaning | Example |
|---|---|---|
true | Validation passed | obj => obj.age >= 18 |
false | Validation failed (generic) | obj => obj.price > 0 |
string | Validation failed with message | obj => obj.age >= 18 || 'Must be 18+' |
const user = UserBuilder.create()
.name("Alice")
.email("alice@example.com")
.age(16)
.addValidator((obj) => {
if (obj.age === undefined) return true; // Optional field
return obj.age >= 18 || "User must be at least 18 years old";
})
.build();
const user = UserBuilder.create()
.name("Charlie")
.email("charlie@example.com")
.addValidator((obj) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(obj.email || "") || "Invalid email format";
})
.build();
const user = UserBuilder.create()
.name("Diana")
.email("diana@example.com")
.password("SecurePass123!")
.addValidator((obj) => {
if (!obj.password) return true; // Skip if not set
const hasUpperCase = /[A-Z]/.test(obj.password);
const hasLowerCase = /[a-z]/.test(obj.password);
const hasNumber = /[0-9]/.test(obj.password);
const hasSpecialChar = /[!@#$%^&*]/.test(obj.password);
const isLongEnough = obj.password.length >= 8;
if (!isLongEnough) return "Password must be at least 8 characters";
if (!hasUpperCase) return "Password must contain an uppercase letter";
if (!hasLowerCase) return "Password must contain a lowercase letter";
if (!hasNumber) return "Password must contain a number";
if (!hasSpecialChar) return "Password must contain a special character";
return true;
})
.build();
type Order = {
totalAmount: number;
discountAmount?: number;
finalAmount: number;
};
class OrderBuilder extends CeriosBuilder<Order> {
// ... methods
}
const order = OrderBuilder.create()
.totalAmount(100)
.discountAmount(20)
.finalAmount(80)
.addValidator((obj) => {
const expected = (obj.totalAmount || 0) - (obj.discountAmount || 0);
return obj.finalAmount === expected || `Final amount must be ${expected} (total - discount)`;
})
.build();
type Employee = {
firstName: string;
lastName: string;
employeeType: "full-time" | "contractor";
employeeId?: string;
contractorId?: string;
};
class EmployeeBuilder extends CeriosBuilder<Employee> {
static requiredTemplate: RequiredFieldsTemplate<Employee> = ["firstName", "lastName", "employeeType"];
// ... methods
}
const employee = EmployeeBuilder.create()
.firstName("John")
.lastName("Doe")
.employeeType("full-time")
.employeeId("EMP-12345")
.addValidator((obj) => {
if (obj.employeeType === "full-time") {
return obj.employeeId !== undefined || "Full-time employees must have an employeeId";
}
if (obj.employeeType === "contractor") {
return obj.contractorId !== undefined || "Contractors must have a contractorId";
}
return true;
})
.build();
type Product = {
name: string;
price: number;
quantity?: number;
};
class ProductBuilder extends CeriosBuilder<Product> {
// ... methods
}
const product = ProductBuilder.create()
.name("Widget")
.price(29.99)
.quantity(5)
.addValidator((obj) => obj.price > 0 || "Price must be positive")
.addValidator((obj) => obj.price < 10000 || "Price cannot exceed $10,000")
.addValidator((obj) => {
if (obj.quantity === undefined) return true;
return obj.quantity >= 0 || "Quantity cannot be negative";
})
.build();
build() or buildWithoutCompileTimeValidation()buildUnsafe() and buildPartial() skip all validators// Validators run for these build methods:
builder.build(); // ✅ Runs validators
builder.buildWithoutCompileTimeValidation(); // ✅ Runs validators
// Validators are skipped for these:
builder.buildWithoutRuntimeValidation(); // ❌ Skips validators
builder.buildUnsafe(); // ❌ Skips validators
builder.buildPartial(); // ❌ Skips validators
Validators work alongside the requiredTemplate for comprehensive validation:
type Registration = {
username: string;
email: string;
age?: number;
acceptedTerms?: boolean;
};
class RegistrationBuilder extends CeriosBuilder<Registration> {
static requiredTemplate: RequiredFieldsTemplate<Registration> = ["username", "email"];
// ... methods
}
const registration = RegistrationBuilder.create()
.username("john_doe")
.email("john@example.com")
.age(25)
.acceptedTerms(true)
// Required template validates username and email are present
// Custom validators check business rules
.addValidator((obj) => (obj.username && obj.username.length >= 3) || "Username must be at least 3 characters")
.addValidator((obj) => obj.email?.includes("@") || "Invalid email format")
.addValidator((obj) => !obj.age || obj.age >= 13 || "Must be at least 13 years old")
.addValidator((obj) => obj.acceptedTerms === true || "Must accept terms and conditions")
.build();
// First checks: username and email are set (requiredTemplate)
// Then runs: all custom validators in order
Create reusable validator functions for common patterns:
// Reusable validator functions
const validators = {
email: (obj: Partial<User>) => obj.email?.includes("@") || "Invalid email format",
minAge: (minAge: number) => (obj: Partial<User>) =>
!obj.age || obj.age >= minAge || `Must be at least ${minAge} years old`,
passwordMatch: (obj: Partial<User>) => {
if (!obj.password || !obj.confirmPassword) return true;
return obj.password === obj.confirmPassword || "Passwords do not match";
},
required:
<T, K extends keyof T>(field: K, message?: string) =>
(obj: Partial<T>) =>
obj[field] !== undefined || message || `${String(field)} is required`,
};
// Use them in your builders
const user = UserBuilder.create()
.name("John Doe")
.email("john@example.com")
.age(20)
.password("secret123")
.confirmPassword("secret123")
.addValidator(validators.email)
.addValidator(validators.minAge(18))
.addValidator(validators.passwordMatch)
.build();
Validators are perfect for test data creation with realistic constraints:
describe("User Registration", () => {
const createValidUser = () =>
UserBuilder.create()
.name("Test User")
.email("test@example.com")
.age(25)
.addValidator((obj) => (obj.age ?? 0) >= 18 || "Must be 18+")
.addValidator((obj) => obj.email?.includes("@") || "Invalid email");
test("should accept valid user", () => {
const user = createValidUser().build();
expect(user.age).toBeGreaterThanOrEqual(18);
});
test("should reject underage user", () => {
expect(() => {
createValidUser().age(16).build();
}).toThrow("Must be 18+");
});
test("should reject invalid email", () => {
expect(() => {
createValidUser().email("invalid-email").build();
}).toThrow("Invalid email");
});
});
Cerios Builder is perfect for test data creation:
describe("User Service", () => {
test("should create user with valid data", () => {
const userData = UserBuilder.create()
.withRandomId()
.name("Test User")
.email("test@example.com")
.addRole("tester")
.age(25)
.build();
const result = userService.createUser(userData);
expect(result.success).toBe(true);
expect(result.user.name).toBe("Test User");
expect(result.user.roles).toContain("tester");
});
test("should handle users without age", () => {
const userData = UserBuilder.create()
.withRandomId()
.name("Ageless User")
.email("ageless@example.com")
.addRole("guest")
.build();
const result = userService.createUser(userData);
expect(result.success).toBe(true);
expect(result.user.age).toBeUndefined();
expect(result.user.roles).toContain("guest");
});
});
// ❌ No compile-time safety
const order = {
Order: {
Details: {
CustomerId: "CUST-001",
TotalAmount: 299.99,
// Oops! Forgot required Status field
ShippingAddress: {
Street: "123 Main St",
// Oops! Forgot required City and Country
},
},
},
};
// ❌ Runtime error waiting to happen
orderService.createOrder(order);
// ✅ Type-safe nested property building
const order = OrderRequestBuilder.createWithDefaults()
.customerId("CUST-001")
.totalAmount(299.99)
.shippingStreet("123 Main St")
.shippingCity("New York")
.shippingCountry("USA")
.build(); // ✅ Both compile-time and runtime validation
// ✅ Clear error messages at runtime
try {
const invalid = OrderRequestBuilder.createWithDefaults().customerId("CUST-002").buildWithoutCompileTimeValidation();
} catch (error) {
console.error(error.message);
// "Missing required fields: Order.Details.TotalAmount,
// Order.Details.ShippingAddress.Street, ..."
}
Base class for all builders.
requiredTemplate?: RequiredFieldsTemplate<T> - Optional array of required field paths for runtime validationsetProperty<K>(key: K, value: T[K]) - Sets a property and returns a new builder instancesetProperties<K>(props: Pick<T, K>) - Sets multiple properties at once and returns a new builder instancesetNestedProperty<P>(path: P, value: PathValue<T, P>) - Sets a deeply nested property using dot notationaddToArrayProperty<K, V>(key: K, value: V) - Adds a value to an array property and returns a new builder instancesetRequiredFields(fields: ReadonlyArray<Path<T>>) - Sets required fields dynamically for this instanceaddValidator(validator: (obj: Partial<T>) => boolean | string) - Adds a custom validator that runs during buildremoveOptionalProperty<K>(key: K) - Removes an optional property from the builderclearOptionalProperties() - Clears all optional properties, keeping only required onesclone() - Creates a clone of the current builder instancestatic from<T>(instance: T) - Creates a new builder from an existing objectbuild() - Builds with both compile-time and runtime validation (recommended)
requiredTemplatebuildWithoutRuntimeValidation() - Builds with compile-time validation only
buildWithoutCompileTimeValidation() - Builds with runtime validation only
requiredTemplatebuildUnsafe() - Builds without any validation
buildPartial() - Builds a partial object
Partial<T> with currently set propertiesType-safe array of paths for defining required fields.
type RequiredFieldsTemplate<T> = ReadonlyArray<Path<T>>;
Example:
static requiredTemplate: RequiredFieldsTemplate<OrderRequest> = [
'Order.Details.CustomerId',
'Order.Details.TotalAmount',
'Order.Details.ShippingAddress.Street',
];
Type utility that generates all valid dot-notation paths for a type.
// For a type like:
type User = {
profile: {
name: string;
email: string;
};
};
// Path<User> includes:
// 'profile' | 'profile.name' | 'profile.email'
Deprecated for direct consumer usage. Prefer BuilderStep, BuilderPreset, BuilderComposer, and BuilderComposerFromFactory.
CeriosBrand<T> remains exported for backward compatibility.
build() - Full Validation (Recommended)Use build() when you want both compile-time and runtime validation. This is the safest option.
class UserBuilder extends CeriosBuilder<User> {
static requiredTemplate: RequiredFieldsTemplate<User> = ["id", "name", "email"];
// ... methods
}
const user = UserBuilder.create().id("123").name("John").email("john@example.com").build(); // ✅ TypeScript + runtime validation
// ❌ TypeScript error - missing email
const invalid1 = UserBuilder.create().id("123").name("John").build();
// ❌ Runtime error - missing email
try {
const invalid2 = UserBuilder.create().id("123").name("John").build();
} catch (error) {
console.error(error.message); // "Missing required fields: email"
}
Use when:
buildWithoutRuntimeValidation() - Compile-Time OnlyUse when you want TypeScript safety but need to skip runtime validation for performance.
const user = UserBuilder.create().id("123").name("John").email("john@example.com").buildWithoutRuntimeValidation(); // ✅ Fast build, no runtime check
// ❌ Still won't compile - TypeScript enforces types
const invalid = UserBuilder.create().id("123").name("John").buildWithoutRuntimeValidation();
Use when:
buildWithoutCompileTimeValidation() - Runtime OnlyUse when building from external data where compile-time checks aren't possible.
function buildFromAPI(data: any) {
const builder = UserBuilder.create();
if (data.id) builder = builder.id(data.id);
if (data.name) builder = builder.name(data.name);
if (data.email) builder = builder.email(data.email);
return builder.buildWithoutCompileTimeValidation(); // Runtime validation
}
// ✅ Valid data passes
const user = buildFromAPI({ id: "123", name: "John", email: "john@example.com" });
// ❌ Invalid data throws
try {
const invalid = buildFromAPI({ id: "123", name: "John" }); // Missing email
} catch (error) {
console.error(error.message); // "Missing required fields: email"
}
Use when:
buildUnsafe() - No ValidationUse only when you're certain the object is valid and need maximum performance.
const user = UserBuilder.create().id("123").buildUnsafe(); // ⚠️ No checks at all
console.log(user); // { id: '123' } - potentially incomplete!
Use when:
buildPartial() - Partial ObjectsUse when you need incomplete objects.
const partial = UserBuilder.create().name("Incomplete User").buildPartial(); // Returns Partial<User>
console.log(partial); // { name: 'Incomplete User' }
Use when:
| Method | Compile-Time Check | Runtime Check | Performance | Safety | Use Case |
|---|---|---|---|---|---|
build() | ✅ | ✅ | Medium | Highest | Production code |
buildWithoutRuntimeValidation() | ✅ | ❌ | Fast | High | Performance-critical |
buildWithoutCompileTimeValidation() | ❌ | ✅ | Medium | Medium | External data |
buildUnsafe() | ❌ | ❌ | Fastest | Lowest | Trusted scenarios only |
buildPartial() | ❌ | ❌ | Fast | N/A | Incomplete objects |
In addition to the standard build methods, @cerios/cerios-builder provides frozen and sealed variants that create immutable objects. These methods help prevent accidental mutations and enforce data integrity.
Before diving into the methods, it's important to understand the difference between frozen and sealed objects:
| Feature | Object.freeze() | Object.seal() |
|---|---|---|
| Add properties | ❌ Prevented | ❌ Prevented |
| Delete properties | ❌ Prevented | ❌ Prevented |
| Modify properties | ❌ Prevented | ✅ Allowed |
| Use case | Complete immutability | Prevent structural changes |
Shallow vs Deep:
buildFrozen() - Shallow FreezeCreates a shallowly frozen object where top-level properties cannot be modified, but nested objects remain mutable.
const user = UserBuilder.create().id("123").name("John").email("john@example.com").buildFrozen(); // Returns Readonly<User>
// ❌ Error: Cannot modify top-level properties
user.name = "Jane"; // TypeError in strict mode
// ⚠️ Nested objects can still be modified (shallow freeze)
user.address.city = "New York"; // Works if address is an object
Use when:
buildDeepFrozen() - Deep FreezeCreates a deeply frozen object where all properties at all levels are immutable.
const user = UserBuilder.create()
.id("123")
.name("John")
.email("john@example.com")
.setNestedProperty("address.city", "Boston")
.buildDeepFrozen(); // Returns DeepReadonly<User>
// ❌ Error: Cannot modify top-level properties
user.name = "Jane"; // TypeError
// ❌ Error: Cannot modify nested properties
user.address.city = "New York"; // TypeError
// ❌ Error: Cannot modify arrays
user.tags.push("new tag"); // TypeError
Use when:
buildSealed() - Shallow SealCreates a shallowly sealed object where properties cannot be added or removed, but existing properties can be modified.
const user = UserBuilder.create().id("123").name("John").email("john@example.com").buildSealed(); // Returns T
// ✅ Can modify existing properties
user.name = "Jane"; // Works!
// ❌ Cannot add new properties
user.newProp = "value"; // TypeError in strict mode
// ❌ Cannot delete properties
delete user.email; // TypeError in strict mode
// ⚠️ Can modify nested objects (shallow seal)
user.address.city = "New York"; // Works if address is an object
Use when:
buildDeepSealed() - Deep SealCreates a deeply sealed object where no properties can be added or removed at any level, but all properties can still be modified.
const user = UserBuilder.create()
.id("123")
.name("John")
.email("john@example.com")
.setNestedProperty("address.city", "Boston")
.buildDeepSealed(); // Returns T
// ✅ Can modify properties at all levels
user.name = "Jane"; // Works
user.address.city = "New York"; // Works
// ❌ Cannot add properties at any level
user.newProp = "value"; // TypeError
user.address.newProp = "value"; // TypeError
// ❌ Cannot add array elements
user.tags.push("new tag"); // TypeError
// ✅ Can modify array elements
user.tags[0] = "updated tag"; // Works
Use when:
| Method | Modify Top-Level | Modify Nested | Add Properties | Delete Properties | Performance |
|---|---|---|---|---|---|
buildFrozen() | ❌ | ✅ | ❌ | ❌ | Fast |
buildDeepFrozen() | ❌ | ❌ | ❌ | ❌ | Slower |
buildSealed() | ✅ | ✅ | ❌ | ❌ | Fast |
buildDeepSealed() | ✅ | ✅ | ❌ | ❌ | Slower |
class ConfigBuilder extends CeriosBuilder<AppConfig> {
static requiredTemplate: RequiredFieldsTemplate<AppConfig> = ["apiUrl", "timeout", "features.authentication"];
static create() {
return new ConfigBuilder({});
}
// ... builder methods
}
const config = ConfigBuilder.create()
.setApiUrl("https://api.example.com")
.setTimeout(5000)
.setNestedProperty("features.authentication", true)
.buildDeepFrozen(); // Configuration should never change
// ❌ Configuration is completely frozen
config.timeout = 10000; // TypeError
config.features.authentication = false; // TypeError
class UserStateBuilder extends CeriosBuilder<UserState> {
static create() {
return new UserStateBuilder({});
}
// ... builder methods
}
const userState = UserStateBuilder.create()
.setUserId("123")
.setStatus("active")
.setLastSeen(new Date())
.buildDeepSealed(); // Structure is fixed, but values can change
// ✅ Can update state values
userState.status = "inactive";
userState.lastSeen = new Date();
// ❌ Cannot add new properties
userState.newField = "value"; // TypeError
class AppStateBuilder extends CeriosBuilder<AppState> {
// ... methods
}
const snapshots: DeepReadonly<AppState>[] = [];
// Capture immutable snapshots
snapshots.push(AppStateBuilder.create().setCurrentUser(user).setOpenDocuments(documents).buildDeepFrozen());
// Snapshots cannot be modified
snapshots[0].currentUser.name = "Changed"; // TypeError
Use buildDeepFrozen():
Use buildDeepSealed():
Use shallow variants (buildFrozen(), buildSealed()):
Avoid immutable builds when:
Use setNestedProperty() for deep structures: Instead of building nested objects separately, use dot notation for better type safety and cleaner code.
Define requiredTemplate for runtime validation: When working with external data or complex validation rules, define a required template.
Choose the right build method:
build() for safetybuildWithoutRuntimeValidation() for performancebuildWithoutCompileTimeValidation() for external databuildUnsafe() only when absolutely necessarybuildDeepFrozen() for configuration and immutable databuildDeepSealed() when you need fixed schemas with mutable valuesUse setRequiredFields() for conditional requirements: When requirements change based on context (e.g., new vs existing records), use dynamic required fields.
Create factory methods: Use static create() and createWithDefaults() methods for better ergonomics:
static createWithDefaults() {
return this.create()
.status('pending')
.createdAt(new Date());
}
Combine with validation libraries: Use runtime validation with libraries like Zod for comprehensive validation:
const data = builder.buildWithoutCompileTimeValidation();
const validated = orderSchema.parse(data); // Zod validation
Keep builders focused: One builder per entity type - don't try to handle multiple unrelated types in one builder.
Use type-safe paths: The RequiredFieldsTemplate type ensures you can only specify valid paths:
static requiredTemplate: RequiredFieldsTemplate<Order> = [
'Details.CustomerId', // ✅ Valid path
'Details.InvalidField', // ❌ TypeScript error
];
CeriosClassBuilder is an experimental builder base class for TypeScript classes (not just types/interfaces). It provides the same fluent, type-safe builder API as CeriosBuilder, but returns actual class instances—preserving methods, decorators, and prototype chains.
CeriosBuilder.setNestedProperty for deep class graphs.import { CeriosClassBuilder } from "@cerios/cerios-builder";
class Person {
name!: string;
age!: number;
greet() {
return `Hello, I'm ${this.name}`;
}
}
class PersonBuilder extends CeriosClassBuilder<Person> {
constructor(classConstructor: ClassConstructor<Person> = Person, data: Partial<Person> = {}) {
super(classConstructor, data);
}
static create() {
return new PersonBuilder(Person);
}
name(value: string) {
return this.setProperty("name", value);
}
age(value: number) {
return this.setProperty("age", value);
}
}
const person = PersonBuilder.create().name("Alice").age(30).build();
console.log(person.greet()); // "Hello, I'm Alice"
console.log(person instanceof Person); // true
CeriosBuilder is the goal.Contributions are welcome! Please feel free to submit a Pull Request.
MIT © Ronald Veth - Cerios
FAQs
A TypeScript builder pattern library providing compile-time type safety for object construction with method chaining and required field validation
The npm package @cerios/cerios-builder receives a total of 20 weekly downloads. As such, @cerios/cerios-builder popularity was classified as not popular.
We found that @cerios/cerios-builder demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Product
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.

Product
Socket MCP now lets AI assistants review org alerts, investigate threats using the Socket threat feed, and inspect package files in addition to dependency scoring.

Product
Socket Firewall blocks malicious VS Code and Open VSX extensions before install, protecting developers from compromised editor marketplaces.