Typed Builder Pattern in TypeScript
The Builder pattern is a creational design pattern that allows for the step-by-step construction of complex objects. In TypeScript, we can leverage its type system to create highly robust and type-safe builders. This challenge focuses on implementing a builder for a hypothetical User object, ensuring that each step of the building process is strictly typed and that the final object can only be created after all required fields are set.
Problem Description
Your task is to implement a TypeScript builder for a User object. The User object has the following properties:
id: number (required)username: string (required)email: string (required)isActive: boolean (optional, defaults tofalse)registrationDate: Date (optional, defaults tonew Date())
The builder should enforce the following:
- Step-by-Step Construction: The builder should guide the user through setting the required properties (
id,username,email) sequentially. - Type Safety: Each method for setting a property should return a new builder instance with an updated type that reflects the properties already set.
- Optional Properties: Methods for setting optional properties (
isActive,registrationDate) should be available at any point after the required properties are set. - Finalization: The
build()method should only be callable when all required properties have been set. - Immutability: Each setter method should return a new builder instance, ensuring the original builder remains unchanged.
You will need to define a way to represent the state of the builder (which properties have been set) using TypeScript's type system. This will likely involve conditional types or mapped types to progressively refine the builder's type.
Examples
Example 1: A valid user creation.
// Assuming you have implemented the UserBuilder class and related types
const user = new UserBuilder()
.setId(1)
.setUsername("alice")
.setEmail("alice@example.com")
.build();
console.log(user);
// Expected Output:
// {
// id: 1,
// username: "alice",
// email: "alice@example.com",
// isActive: false,
// registrationDate: <current date>
// }
Example 2: User creation with optional fields.
const now = new Date();
const activeUser = new UserBuilder()
.setId(2)
.setUsername("bob")
.setEmail("bob@example.com")
.setIsActive(true)
.setRegistrationDate(now)
.build();
console.log(activeUser);
// Expected Output:
// {
// id: 2,
// username: "bob",
// email: "bob@example.com",
// isActive: true,
// registrationDate: <the 'now' date object>
// }
Example 3: Attempting to build before setting all required fields (should result in a TypeScript compile-time error).
// This code should NOT compile
const incompleteUserBuilder = new UserBuilder()
.setId(3)
.setUsername("charlie");
// The following line would cause a TypeScript error because email is not set
// const incompleteUser = incompleteUserBuilder.build();
Constraints
- The
Userobject must have the propertiesid,username,email,isActive, andregistrationDatewith the specified types and default values. - The builder must enforce setting
id,username, andemailin any order, but all must be set beforebuild()is called. - The builder methods for setting properties should be chainable.
- The
build()method should return aUserobject. - Your solution must be purely in TypeScript.
Notes
Consider how you can use TypeScript's type system to track which required properties have been set. This might involve defining multiple types that represent different stages of the builder's construction. Think about how you can use generics and conditional types to achieve the desired type-safety. You might also consider using an object to hold the builder's state internally.