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
-
UserClass: Define aUserclass with the following properties:email:string(required)firstName:string(optional)lastName:string(optional)age:number(optional)isAdmin:boolean(defaults tofalseif not set)
-
UserBuilderClass: Create aUserBuilderclass that will be responsible for constructingUserobjects.- The builder should accept the
emailas a mandatory argument during its instantiation. - Implement fluent setter methods for each optional property (
firstName,lastName,age,isAdmin). Each setter method should returnthis(the builder instance) to allow for chaining. - Implement a
build()method that returns a fully constructedUserobject. This method should:- Set the
isAdminproperty tofalseif it hasn't been explicitly set by the builder. - Perform basic validation: ensure
emailis provided. If not, throw an error.
- Set the
- The builder should accept the
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 theUserBuilderwas instantiated without anemail(although the prompt specifies email is mandatory during instantiation, consider how the builder handles it if the constructor logic were slightly different). - Ensure
isAdmindefaults tofalse.
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
emailproperty must be a non-empty string. - The
ageproperty, if set, must be a non-negative number. - The
firstNameandlastNameproperties, if set, must be non-empty strings. - The
build()method should throw anErrorwith 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
undefinedornullby the user. Thebuildmethod should decide how to interpret these. For this problem, we will treat explicitly setting them toundefinedas if they were not set. - Think about the return type of your setter methods.
- The
Userclass itself can be a simple class or interface; the focus is on theUserBuilder. - You can use default parameters in TypeScript to simplify initial
isAdminhandling, but the builder'sbuildmethod is where explicit defaulting if not set by the builder should be handled.