Value Objects
Value objects are immutable objects that represent validated domain concepts. They encapsulate validation logic and ensure data integrity throughout the application.
Email
Represents a validated email address.
Class Definition
src/domain/value-objects/Email.ts
export class Email {
private readonly value : string ;
constructor ( email : string ) {
if ( ! this . isValid ( email )) {
throw new Error ( `Invalid email: ${ email } ` );
}
this . value = email . toLowerCase (). trim ();
}
private isValid ( email : string ) : boolean {
return / ^ [ ^ \s@ ] + @ [ ^ \s@ ] + \. [ ^ \s@ ] + $ / . test ( email );
}
toString () : string {
return this . value ;
}
}
Constructor
Raw email address to validate and normalize
Behavior:
Validates the email format using regex pattern
Throws an error if the email is invalid
Normalizes the email by converting to lowercase and trimming whitespace
Stores the normalized value immutably
Methods
Returns the normalized email address as a string. Returns: The validated and normalized email address (lowercase, trimmed)Example:
Validation Rules
The email must match the following regex pattern:
/ ^ [ ^ \s @ ] + @ [ ^ \s @ ] +\. [ ^ \s @ ] + $ /
^[^\s@]+ - Starts with one or more characters that are not whitespace or @
@ - Contains exactly one @ symbol
[^\s@]+ - Followed by one or more characters that are not whitespace or @
\. - Contains a period (dot)
[^\s@]+$ - Ends with one or more characters that are not whitespace or @
Usage Examples
Valid Email
Invalid Email
In Use Case
import { Email } from '@/domain/value-objects/Email' ;
try {
const email = new Email ( 'invalid-email' );
} catch ( error ) {
console . error ( error . message );
// Error: Invalid email: invalid-email
}
try {
const email = new Email ( 'user@domain' );
} catch ( error ) {
console . error ( error . message );
// Error: Invalid email: user@domain
}
import { Email } from '@/domain/value-objects/Email' ;
import { createCustomer } from '@/domain/entities/Customer' ;
// From CreateOrderUseCase
async execute ( dto : CreateOrderDto ): Promise < OrderResponseDto > {
const email = new Email ( dto . email ); // Validates email
const customerData = createCustomer ({
name: dto . name ,
email: email . toString (), // Use normalized value
phone: phone . toString (),
});
const customer = await this . customerRepo . create ( customerData );
// ...
}
Phone
Represents a validated phone number.
Class Definition
src/domain/value-objects/Phone.ts
export class Phone {
private readonly value : string ;
constructor ( phone : string ) {
const cleaned = phone . replace ( / \D / g , "" );
if ( ! this . isValid ( cleaned )) {
throw new Error ( `Invalid phone: ${ phone } ` );
}
this . value = cleaned ;
}
private isValid ( phone : string ) : boolean {
return phone . length >= 10 && phone . length <= 15 ;
}
toString () : string {
return this . value ;
}
}
Constructor
Raw phone number to validate and normalize
Behavior:
Removes all non-digit characters (spaces, dashes, parentheses, etc.)
Validates that the cleaned number has between 10-15 digits
Throws an error if the phone number is invalid
Stores the cleaned value immutably
Methods
Returns the normalized phone number as a string. Returns: The validated phone number containing only digitsExample: const phone = new Phone ( '+1 (555) 123-4567' );
console . log ( phone . toString ()); // "15551234567"
Validation Rules
The phone number must meet the following criteria after removing non-digits:
Must be between 10 and 15 digits (inclusive)
Valid Examples:
10 digits: "1234567890"
11 digits: "12345678901" (e.g., with country code)
15 digits: "123456789012345"
Invalid Examples:
Too short: "123456789" (9 digits)
Too long: "1234567890123456" (16 digits)
Usage Examples
Valid Phone
Invalid Phone
In Use Case
import { Phone } from '@/domain/value-objects/Phone' ;
try {
// Various formats are accepted
const phone1 = new Phone ( '+1 (555) 123-4567' );
console . log ( phone1 . toString ()); // "15551234567"
const phone2 = new Phone ( '555-123-4567' );
console . log ( phone2 . toString ()); // "5551234567"
const phone3 = new Phone ( '5551234567' );
console . log ( phone3 . toString ()); // "5551234567"
} catch ( error ) {
console . error ( error . message );
}
import { Phone } from '@/domain/value-objects/Phone' ;
try {
const phone = new Phone ( '123' ); // Too short
} catch ( error ) {
console . error ( error . message );
// Error: Invalid phone: 123
}
try {
const phone = new Phone ( '12345678901234567890' ); // Too long
} catch ( error ) {
console . error ( error . message );
// Error: Invalid phone: 12345678901234567890
}
import { Phone } from '@/domain/value-objects/Phone' ;
import { createCustomer } from '@/domain/entities/Customer' ;
// From CreateOrderUseCase
async execute ( dto : CreateOrderDto ): Promise < OrderResponseDto > {
const phone = new Phone ( dto . phone ); // Validates and cleans phone
const customerData = createCustomer ({
name: dto . name ,
email: email . toString (),
phone: phone . toString (), // Use cleaned value
});
const customer = await this . customerRepo . create ( customerData );
// ...
}
Value Object Characteristics
Value objects are immutable. Once created, their internal state cannot be changed. This ensures data integrity and prevents unexpected side effects. const email = new Email ( '[email protected] ' );
// email.value is private and readonly
// No methods exist to modify the internal value
Value objects validate their input in the constructor and throw errors for invalid data. This ensures that invalid objects cannot exist in the system. try {
const email = new Email ( 'invalid' );
} catch ( error ) {
// Handle validation error
console . error ( 'Email validation failed' );
}
Value objects normalize their input to a canonical form. This ensures consistency throughout the application.
Validation logic is encapsulated within the value object, keeping domain rules close to the data they govern. // Validation logic is hidden in private methods
// Business logic doesn't need to know HOW validation works
const email = new Email ( userInput );
const phone = new Phone ( userInput );