BaseValueObject
All value objects extend BaseValueObject<T> and validate in their create() factory method:
import { BaseValueObject } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
import { InvariantViolationError } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
class Email extends BaseValueObject<string> {
private static readonly EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
static create(value: Email['value']): Email {
if (!Email.EMAIL_REGEX.test(value)) {
throw new InvariantViolationError({
message: 'Invalid email format',
code: 'INVALID_EMAIL',
});
}
return new Email(value);
}
get domain(): string {
return this.value.split('@')[1];
}
}
Key features:
- Immutable: Value is set at construction and cannot be changed
- Self-validating: Validation runs in
create()before construction - Comparable: Built-in
equals()method with deep equality - Type inference: Use
ClassName['value']for input type
Built-in Value Objects
The library provides base value objects you can extend:
Text Types
import {
BaseTextVo, // Configurable text with static constraints
BaseShortTextVo, // 1-100 characters
BaseMediumTextVo, // 1-500 characters
BaseLongTextVo, // 1-5000 characters
} from '@cosmneo/onion-lasagna/backend/core/onion-layers';
// Create custom text VO with constraints
class ProductName extends BaseTextVo {
static override defaultMinLength = 1;
static override defaultMaxLength = 50;
}
class SkuCode extends BaseTextVo {
static override defaultMinLength = 3;
static override defaultMaxLength = 20;
static override defaultPattern = /^[A-Z0-9-]+$/;
}
Info:
Text VOs use new this(value) internally, so subclass instances are created correctly at runtime. However, TypeScript still infers the return type as the base class. For strict type safety, you can override create():
class ProductName extends BaseTextVo {
static override defaultMinLength = 1;
static override defaultMaxLength = 50;
static override create(value: ProductName['value']): ProductName {
// Let parent validate, then cast to correct type
BaseTextVo.create.call(this, value);
return new ProductName(value);
}
}
Identifiers
import {
BaseUuidV4Vo, // UUID v4 format (random)
BaseUuidV7Vo, // UUID v7 format (time-ordered)
} from '@cosmneo/onion-lasagna/backend/core/onion-layers';
// Usage
const id = BaseUuidV4Vo.generate(); // Generate new
const parsed = BaseUuidV4Vo.create(uuidStr); // Validate existing
Contact
import { BaseEmailVo } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
const email = BaseEmailVo.create('user@example.com');
Pagination
import { BasePaginationVo } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
const page = BasePaginationVo.create({ page: 1, pageSize: 20 });
// Properties
page.page; // 1
page.pageSize; // 20
page.offset; // 0 (calculated: (page - 1) * pageSize)
// Custom max page size
class AdminPaginationVo extends BasePaginationVo {
static override get maxPageSize(): number {
return 500;
}
}
Auditing
import {
BaseAuditByVo, // createdBy, updatedBy (optional UUIDs)
BaseAuditOnVo, // createdAt, updatedAt (with invariant check)
} from '@cosmneo/onion-lasagna/backend/core/onion-layers';
// Create with current timestamp
const auditOn = BaseAuditOnVo.now();
// Create with specific dates
const auditOn = BaseAuditOnVo.create({
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-15'),
});
// Update timestamp (returns new immutable instance)
const updated = auditOn.update();
Note: BaseAuditOnVo enforces that updatedAt cannot be before createdAt.
Creating Custom Value Objects
Simple Identifier
import { v7 } from 'uuid';
import { BaseUuidV7Vo } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
class OrderId extends BaseUuidV7Vo {
// Override generate() to return correct type
static override generate(): OrderId {
return new OrderId(v7());
}
// IMPORTANT: Override create() to return correct type
static override create(value: OrderId['value']): OrderId {
const validated = BaseUuidV7Vo.create(value);
return new OrderId(validated.value);
}
}
Warning:
Always override create() for UUID subclasses. Without this override, OrderId.create(uuid) returns BaseUuidV7Vo instead of OrderId, breaking type safety and instanceof checks.
Value Object with Custom Validation
import { BaseValueObject } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
import { InvariantViolationError } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
class PhoneNumber extends BaseValueObject<string> {
private static readonly PHONE_REGEX = /^\+[1-9]\d{1,14}$/;
static create(value: PhoneNumber['value']): PhoneNumber {
if (!PhoneNumber.PHONE_REGEX.test(value)) {
throw new InvariantViolationError({
message: 'Invalid phone number format (E.164 required)',
code: 'INVALID_PHONE',
});
}
return new PhoneNumber(value);
}
get countryCode(): string {
return this.value.slice(1, 3);
}
}
Composite Value Object
import { BaseValueObject } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
import { InvariantViolationError } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
interface AddressData {
street: string;
city: string;
postalCode: string;
country: string;
}
class Address extends BaseValueObject<AddressData> {
static create(data: Address['value']): Address {
if (!data.street || data.street.trim().length === 0) {
throw new InvariantViolationError({
message: 'Street is required',
code: 'INVALID_ADDRESS',
});
}
if (!data.country || data.country.length !== 2) {
throw new InvariantViolationError({
message: 'Country must be 2-letter ISO code',
code: 'INVALID_COUNTRY',
});
}
return new Address(data);
}
get street(): string { return this.value.street; }
get city(): string { return this.value.city; }
get fullAddress(): string {
return `${this.value.street}, ${this.value.city}, ${this.value.country}`;
}
}
Enum-like Value Object
import { BaseValueObject } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
type OrderStatusValue = 'pending' | 'confirmed' | 'shipped' | 'cancelled';
class OrderStatus extends BaseValueObject<OrderStatusValue> {
static pending(): OrderStatus {
return new OrderStatus('pending');
}
static confirmed(): OrderStatus {
return new OrderStatus('confirmed');
}
static shipped(): OrderStatus {
return new OrderStatus('shipped');
}
static cancelled(): OrderStatus {
return new OrderStatus('cancelled');
}
isPending(): boolean { return this.value === 'pending'; }
isShipped(): boolean { return this.value === 'shipped'; }
isModifiable(): boolean {
return this.isPending() || this.value === 'confirmed';
}
}
Equality Comparison
BaseValueObject provides deep equality via equals():
const email1 = BaseEmailVo.create('user@example.com');
const email2 = BaseEmailVo.create('user@example.com');
const email3 = BaseEmailVo.create('other@example.com');
email1.equals(email2); // true (same value)
email1.equals(email3); // false (different value)
The comparison handles:
- Primitive values
- Nested objects
- Arrays
- Date objects
Rules
- Use factory methods (
create,generate) - never call constructor directly - Validate in
create()and throwInvariantViolationErrorfor invalid data - Use
ClassName['value']for type inference in create parameters - Add convenience getters for derived values
- Extend built-in VOs when possible (BaseEmailVo, BaseUuidV7Vo, etc.)
- Don't mutate after construction
- Don't use for complex objects (use Entities/Aggregates)