Hone logo
Hone
Problems

Advanced Builder Pattern Implementation in TypeScript

This challenge focuses on implementing a sophisticated builder pattern in TypeScript to construct complex objects with multiple optional configurations. The builder pattern promotes readability and maintainability by separating the construction of a complex object from its representation. This exercise will test your understanding of how to create flexible and extensible builders.

Problem Description

You are tasked with creating a User object with various optional properties such as email, firstName, lastName, age, and isAdmin. You need to implement a builder pattern for this User class. The builder should allow for chaining method calls to set these optional properties and then finalize the construction of the User object.

Key Requirements

  1. User Class: Define a User class with the following properties:

    • email: string (required)
    • firstName: string (optional)
    • lastName: string (optional)
    • age: number (optional)
    • isAdmin: boolean (defaults to false if not set)
  2. UserBuilder Class: Create a UserBuilder class that will be responsible for constructing User objects.

    • The builder should accept the email as a mandatory argument during its instantiation.
    • Implement fluent setter methods for each optional property (firstName, lastName, age, isAdmin). Each setter method should return this (the builder instance) to allow for chaining.
    • Implement a build() method that returns a fully constructed User object. This method should:
      • Set the isAdmin property to false if it hasn't been explicitly set by the builder.
      • Perform basic validation: ensure email is provided. If not, throw an error.

Expected Behavior

The builder should allow users to construct User objects with varying combinations of optional properties. The build() method should encapsulate the final creation and validation logic.

Edge Cases

  • What happens if build() is called without setting any optional properties?
  • What happens if build() is called when the UserBuilder was instantiated without an email (although the prompt specifies email is mandatory during instantiation, consider how the builder handles it if the constructor logic were slightly different).
  • Ensure isAdmin defaults to false.

Examples

Example 1:

// Input code for demonstration
const user1 = new UserBuilder("test@example.com")
  .setFirstName("John")
  .setLastName("Doe")
  .setAge(30)
  .build();

console.log(user1);
Output:
{
  "email": "test@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "age": 30,
  "isAdmin": false
}
Explanation: A user object is created with email, first name, last name, and age. isAdmin defaults to false.

Example 2:

// Input code for demonstration
const user2 = new UserBuilder("admin@example.com")
  .setIsAdmin(true)
  .build();

console.log(user2);
Output:
{
  "email": "admin@example.com",
  "firstName": undefined,
  "lastName": undefined,
  "age": undefined,
  "isAdmin": true
}
Explanation: A user object is created with only an email and isAdmin set to true. Other optional properties remain undefined.

Example 3:

// Input code for demonstration
try {
  const user3 = new UserBuilder("invalid@example.com")
    // No build call, demonstrating missing finalization is not the issue
    .setAge(25);
  // Intentionally not calling build() to test potential issues
  console.log("User builder created but not built.");
} catch (error) {
  console.error(error.message);
}

try {
  const user4 = new UserBuilder("malformed@example.com")
    .setFirstName("Jane")
    .setAge(undefined as any); // Explicitly setting to undefined for testing
  user4.build();
} catch (error) {
  console.error(error.message);
}

try {
  // This should fail due to missing email during instantiation
  // (though the constructor should prevent this, let's assume it does for the purpose of the challenge)
  // For this exercise, focus on the build() validation.
  // The actual instantiation error if the constructor were less strict is not the primary focus here.
  const incompleteBuilder = new UserBuilder(null as any); // Simulating missing email during instantiation
  incompleteBuilder.build();
} catch (error) {
  console.error(error.message); // Expecting "Email is required."
}
Output:
User builder created but not built.
Email is required.

Explanation: Example 3 demonstrates error handling. The first part shows that simply creating a builder instance doesn't cause an error if build() isn't called. The second part shows that attempting to build a user with an invalid age (explicitly undefined) would be handled by the build method's logic, leading to an error if the age should not be undefined when built (though in this specific prompt, undefined age is acceptable if not set). The third part simulates an error if the email was not provided during builder instantiation, which the build() method should catch and report. (Note: The UserBuilder constructor is intended to prevent null email, so the third try-catch is more about illustrating the desired outcome of validation if it somehow slipped through, or if the validation were on the build method for a potentially incomplete builder.)

Constraints

  • The email property must be a non-empty string.
  • The age property, if set, must be a non-negative number.
  • The firstName and lastName properties, if set, must be non-empty strings.
  • The build() method should throw an Error with a clear message if validation fails.
  • The builder implementation should be efficient; excessive object creation or copying should be avoided where possible.

Notes

  • Consider how to handle optional properties that are explicitly set to undefined or null by the user. The build method should decide how to interpret these. For this problem, we will treat explicitly setting them to undefined as if they were not set.
  • Think about the return type of your setter methods.
  • The User class itself can be a simple class or interface; the focus is on the UserBuilder.
  • You can use default parameters in TypeScript to simplify initial isAdmin handling, but the builder's build method is where explicit defaulting if not set by the builder should be handled.
Loading editor...
typescript