Documentation Index Fetch the complete documentation index at: https://woltz.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
What is a Value Object?
A Value Object is an immutable wrapper around a primitive value (string, number, boolean, or Date) that encapsulates domain behavior and validation. Two Value Objects with the same value are considered equal.
import { ValueObject } from "@woltz/rich-domain" ;
class Email extends ValueObject < string > {
getDomain () : string {
return this . value . split ( "@" )[ 1 ];
}
isBusinessEmail () : boolean {
const freeProviders = [ "gmail.com" , "yahoo.com" , "hotmail.com" ];
return ! freeProviders . includes ( this . getDomain ());
}
}
const email = new Email ( "john@example.com" );
console . log ( email . value ); // "john@example.com"
console . log ( email . getDomain ()); // "example.com"
console . log ( email . isBusinessEmail ()); // true
Key Characteristics
Primitive Values Only Value Objects wrap a single primitive value: string, number, boolean, or Date.
Immutable Once created, a Value Object cannot be changed. Use clone() to create new instances.
Equality by Value Two Value Objects are equal if their primitive values are equal.
Self-Contained Value Objects contain all the behavior related to the concept they represent.
Creating Value Objects
Value Objects accept only primitive types:
// ✅ Valid primitive types
class Age extends ValueObject < number > {}
class Email extends ValueObject < string > {}
class IsActive extends ValueObject < boolean > {}
class BirthDate extends ValueObject < Date > {}
// ❌ Invalid - cannot use complex objects
class Address extends ValueObject <{ street : string ; city : string }> {}
// Error: Value Objects must use primitive types
Basic Usage
class UserId extends ValueObject < string > {}
const userId = new UserId ( "user-123" );
console . log ( userId . value ); // "user-123"
// Value objects are immutable
userId . value = "user-456" ; // ❌ Error: Cannot assign to read only property
Immutability
Value Objects are frozen on creation. To “change” a Value Object, use the clone() method:
class Price extends ValueObject < number > {
addTax ( taxRate : number ) : Price {
const newValue = this . value * ( 1 + taxRate );
return this . clone ( newValue );
}
format () : string {
return new Intl . NumberFormat ( "en-US" , {
style: "currency" ,
currency: "USD" ,
}). format ( this . value );
}
}
const price = new Price ( 99.99 );
const priceWithTax = price . addTax ( 0.08 );
console . log ( price . value ); // 99.99 (unchanged)
console . log ( priceWithTax . value ); // 107.99 (new instance)
console . log ( priceWithTax . format ()); // "$107.99"
Equality
Value Objects are compared by their primitive values:
const email1 = new Email ( "john@example.com" );
const email2 = new Email ( "john@example.com" );
const email3 = new Email ( "jane@example.com" );
email1 . equals ( email2 ); // true - same value
email1 . equals ( email3 ); // false - different value
Validation
Add schema validation to ensure Value Objects are always valid:
import { z } from "zod" ;
import { ValueObject } from "@woltz/rich-domain" ;
const emailSchema = z . string (). email ( "Invalid email format" );
class Email extends ValueObject < string > {
protected static validation = {
schema: emailSchema ,
config: {
onCreate: true ,
throwOnError: true ,
},
};
getDomain () : string {
return this . value . split ( "@" )[ 1 ];
}
}
// Valid
const email = new Email ( "john@example.com" );
// Invalid - throws ValidationError
const invalid = new Email ( "not-an-email" );
// ValidationError: Invalid email format
Lifecycle Hooks
Value Objects support lifecycle hooks for custom validation and side effects:
import {
ValueObject ,
VOHooks ,
throwValidationError ,
} from "@woltz/rich-domain" ;
class Age extends ValueObject < number > {
protected static hooks : VOHooks < number , Age > = {
onBeforeCreate : ( value ) => {
console . log ( `Creating age: ${ value } ` );
},
rules : ( ageObject ) => {
if ( ageObject . value < 0 ) {
throwValidationError ( "value" , "Age cannot be negative" );
}
if ( ageObject . value > 150 ) {
throwValidationError ( "value" , "Age exceeds maximum limit" );
}
},
};
isAdult () : boolean {
return this . value >= 18 ;
}
isMinor () : boolean {
return this . value < 18 ;
}
}
const age = new Age ( 25 );
console . log ( age . isAdult ()); // true
// Invalid - throws ValidationError
const invalidAge = new Age ( - 5 );
// ValidationError: Age cannot be negative
Available Hooks
onBeforeCreate : Called before validation, receives the primitive value
rules : Custom business rules, receives the ValueObject instance
Common Examples
Email
import { z } from "zod" ;
const emailSchema = z . string (). email ();
class Email extends ValueObject < string > {
protected static validation = {
schema: emailSchema ,
};
getDomain () : string {
return this . value . split ( "@" )[ 1 ];
}
getUsername () : string {
return this . value . split ( "@" )[ 0 ];
}
isBusinessEmail () : boolean {
const freeProviders = [ "gmail.com" , "yahoo.com" , "hotmail.com" ];
return ! freeProviders . includes ( this . getDomain ());
}
}
const email = new Email ( "john.doe@example.com" );
console . log ( email . getDomain ()); // "example.com"
console . log ( email . getUsername ()); // "john.doe"
console . log ( email . isBusinessEmail ()); // true
Price
class Price extends ValueObject < number > {
protected static hooks : VOHooks < number , Price > = {
rules : ( price ) => {
if ( price . value < 0 ) {
throwValidationError ( "value" , "Price cannot be negative" );
}
},
};
addTax ( taxRate : number ) : Price {
return this . clone ( this . value * ( 1 + taxRate ));
}
discount ( percentage : number ) : Price {
return this . clone ( this . value * ( 1 - percentage / 100 ));
}
format ( currency : string = "USD" ) : string {
return new Intl . NumberFormat ( "en-US" , {
style: "currency" ,
currency ,
}). format ( this . value );
}
}
const price = new Price ( 99.99 );
const withTax = price . addTax ( 0.08 );
const discounted = price . discount ( 10 );
console . log ( price . format ()); // "$99.99"
console . log ( withTax . format ()); // "$107.99"
console . log ( discounted . format ()); // "$89.99"
Percentage
class Percentage extends ValueObject < number > {
protected static hooks : VOHooks < number , Percentage > = {
rules : ( percentage ) => {
if ( percentage . value < 0 || percentage . value > 100 ) {
throwValidationError ( "value" , "Percentage must be between 0 and 100" );
}
},
};
toDecimal () : number {
return this . value / 100 ;
}
format () : string {
return ` ${ this . value } %` ;
}
apply ( amount : number ) : number {
return amount * this . toDecimal ();
}
}
const discount = new Percentage ( 15 );
console . log ( discount . format ()); // "15%"
console . log ( discount . toDecimal ()); // 0.15
console . log ( discount . apply ( 100 )); // 15
Slug
import { z } from "zod" ;
const slugSchema = z . string (). regex ( / ^ [ a-z0-9- ] + $ / );
class Slug extends ValueObject < string > {
protected static validation = {
schema: slugSchema ,
};
static fromTitle ( title : string ) : Slug {
const slug = title
. toLowerCase ()
. replace ( / [ ^ a-z0-9 ] + / g , "-" )
. replace ( / ^ - | - $ / g , "" );
return new Slug ( slug );
}
toUrl ( baseUrl : string ) : string {
return ` ${ baseUrl } / ${ this . value } ` ;
}
}
const slug = Slug . fromTitle ( "Hello World!" );
console . log ( slug . value ); // "hello-world"
console . log ( slug . toUrl ( "https://example.com" )); // "https://example.com/hello-world"
Serialization
ValueObjects automatically serialize to their primitive values:
const email = new Email ( "john@example.com" );
const json = email . toJSON ();
// "john@example.com" (just the string value)
// In entities, ValueObjects are automatically unwrapped
class User extends Entity <{ id : Id ; email : Email }> {}
const user = new User ({ id: new Id (), email: new Email ( "john@example.com" ) });
const userJson = user . toJSON ();
// { id: "...", email: "john@example.com" }