Scaffold Value Object
Generate a DDD Value Object with immutability, structural equality, and validation.
Triggers
- •"scaffold value object"
- •"create value object"
- •"create vo"
- •"new value object"
Instructions
Step 1: Gather Information
Ask the student for:
- •Value Object name (e.g.,
Money,EmailAddress,DateRange,Address) - •Properties with types (e.g.,
amount: number,currency: string) - •Validation rules (e.g., "amount must be positive", "currency must be 3 letters")
If the student provides a name only, suggest reasonable properties based on common DDD examples.
Step 2: Determine Output Location
Check the current working directory:
- •If in a
before/directory, create files there - •If in a lesson directory, create in
before/ - •Otherwise, create in the current directory
Step 3: Generate Value Object File
Create {value-object-name}.ts (kebab-case filename):
typescript
import { z } from 'zod';
// === Validation Schema ===
const {ValueObject}Schema = z.object({
// Properties with Zod validation
});
type {ValueObject}Props = z.infer<typeof {ValueObject}Schema>;
// === Result Type ===
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
// === Value Object ===
export class {ValueObject} {
private constructor(private readonly props: Readonly<{ValueObject}Props>) {
Object.freeze(this);
}
// Factory method
static create(props: {ValueObject}Props): Result<{ValueObject}> {
const validation = {ValueObject}Schema.safeParse(props);
if (!validation.success) {
return {
success: false,
error: new Error(validation.error.errors.map(e => e.message).join(', ')),
};
}
return { success: true, value: new {ValueObject}(validation.data) };
}
// Property accessors (read-only)
// Structural equality — two VOs are equal if all properties match
equals(other: {ValueObject}): boolean {
return (
// Compare each property
JSON.stringify(this.props) === JSON.stringify(other.props)
);
}
// Return a new instance with modified properties (immutability)
with(overrides: Partial<{ValueObject}Props>): Result<{ValueObject}> {
return {ValueObject}.create({ ...this.props, ...overrides });
}
// String representation
toString(): string {
return `{ValueObject}(${JSON.stringify(this.props)})`;
}
}
Step 4: Generate Test File
Create {value-object-name}.test.ts:
typescript
import { describe, it, expect } from 'vitest';
import { {ValueObject} } from './{value-object-name}';
describe('{ValueObject}', () => {
describe('create', () => {
it('should create with valid properties', () => {
const result = {ValueObject}.create({
// valid props
});
expect(result.success).toBe(true);
});
it('should fail with invalid properties', () => {
const result = {ValueObject}.create({
// invalid props
});
expect(result.success).toBe(false);
});
});
describe('equality', () => {
it('should be equal when all properties match', () => {
const vo1 = {ValueObject}.create({ /* props */ });
const vo2 = {ValueObject}.create({ /* same props */ });
if (vo1.success && vo2.success) {
expect(vo1.value.equals(vo2.value)).toBe(true);
}
});
it('should not be equal when properties differ', () => {
const vo1 = {ValueObject}.create({ /* props A */ });
const vo2 = {ValueObject}.create({ /* props B */ });
if (vo1.success && vo2.success) {
expect(vo1.value.equals(vo2.value)).toBe(false);
}
});
});
describe('immutability', () => {
it('should return a new instance from with()', () => {
const original = {ValueObject}.create({ /* props */ });
if (original.success) {
const modified = original.value.with({ /* changed prop */ });
expect(modified.success).toBe(true);
if (modified.success) {
expect(original.value.equals(modified.value)).toBe(false);
}
}
});
it('should be frozen', () => {
const result = {ValueObject}.create({ /* props */ });
if (result.success) {
expect(Object.isFrozen(result.value)).toBe(true);
}
});
});
});
Step 5: Explain the Pattern
After generating, briefly explain:
- •No identity — Value Objects have no ID; they are defined entirely by their properties
- •Immutability —
Object.freeze()+readonlyprevent mutation after creation - •Structural equality — two VOs with the same properties are considered equal
- •
with()method — creates a new instance instead of mutating (likeDatevsmoment) - •Factory + validation — ensures the VO is always in a valid state
- •When to use — use for concepts like Money, Email, Address, DateRange — things with no lifecycle
Customization Notes
- •Replace placeholder properties with the student's actual properties
- •For simple single-value VOs (e.g., EmailAddress), simplify to a single property
- •Add domain-specific methods (e.g.,
Money.add(),DateRange.overlaps()) - •For complex equality, replace JSON.stringify with property-by-property comparison
- •Consider adding
fromPrimitive()/toPrimitive()for serialization