createValueObject(config)
Creates a Value Object — an object defined by its attributes, not by identity. Two Value Objects with identical props are equal. Props are frozen — immutable from the outside.
import { z } from 'zod';
import { createValueObject } from '@sotajs/ddd';
const MoneySchema = z.object({
amount: z.number(),
currency: z.string().length(3),
});
type MoneyProps = z.infer<typeof MoneySchema>;
const Money = createValueObject({
schema: MoneySchema,
actions: {
add(state, other: MoneyProps) {
if (state.currency !== other.currency)
throw new Error('Currency mismatch');
state.amount += other.amount;
},
},
computed: {
formatted(state) {
return `${state.amount} ${state.currency}`;
},
},
});
type Money = ReturnType<typeof Money.create>;
const a = Money.create({ amount: 100, currency: 'USD' });
const b = Money.create({ amount: 100, currency: 'USD' });
a.equals(b); // true — same attributes
a.actions.add({ amount: 50, currency: 'USD' });
a.formatted; // '150 USD'
equals()usesJSON.stringifycomparison — structural, not referential. Two instances created at different times with the same data are equal. Source: value-object.ts:101-103
Config
| Field | Required | Description |
|---|---|---|
schema | No* | Zod/Valibot schema. |
type | No* | ArkType type. |
validate | No* | Generic validate function. |
actions | No | Optional. Domain actions that mutate a draft copy of state. |
computed | No | Derived getters. |
*One of
schema,type, orvalidateis required.
Instance API
| Member | Type | Description |
|---|---|---|
VO.create(data) | ValueObject instance | Validates and creates. |
vo.props | Readonly<TProps> | Frozen snapshot of current state. |
vo.actions.* | (...args) => void | Action methods (if configured). |
vo.equals(other) | boolean | Structural equality via JSON comparison. |
Structural equality
const a = Money.create({ amount: 100, currency: 'USD' });
const b = Money.create({ amount: 100, currency: 'USD' });
const c = Money.create({ amount: 200, currency: 'USD' });
a.equals(b); // true
a.equals(c); // false
Equality uses
JSON.stringify. This means property order matters,undefinedvalues are stripped, andDate/BigIntobjects may behave unexpectedly. For most Value Objects this is sufficient. If you need custom equality, implement it outside the VO.
Actions are optional
A Value Object without actions is perfectly valid:
const Point = createValueObject({
schema: z.object({ x: z.number(), y: z.number() }),
});
const p = Point.create({ x: 1, y: 2 });
// p.actions is an empty object — no mutation surface
Value Object vs Entity
| Entity | Value Object | |
|---|---|---|
| Identity | Has id — equality by identity | No id — equality by attributes |
| Mutability | Mutable through actions | Mutable through optional actions |
| Use case | ”A specific user" | "An amount of money” |
equals() | Compares id | Compares all attributes |
Pitfalls
- Equality is JSON-based. Dates become strings, BigInt breaks, property order matters.
- No nominal distinction. Two Value Objects with identical shapes are TypeScript-compatible. Use branded types if you need
Email ≠ Usernameat compile time. - Props are frozen. To “change” a VO, use an action (which mutates a draft copy internally).