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() uses JSON.stringify comparison — structural, not referential. Two instances created at different times with the same data are equal. Source: value-object.ts:101-103


Config

FieldRequiredDescription
schemaNo*Zod/Valibot schema.
typeNo*ArkType type.
validateNo*Generic validate function.
actionsNoOptional. Domain actions that mutate a draft copy of state.
computedNoDerived getters.

*One of schema, type, or validate is required.


Instance API

MemberTypeDescription
VO.create(data)ValueObject instanceValidates and creates.
vo.propsReadonly<TProps>Frozen snapshot of current state.
vo.actions.*(...args) => voidAction methods (if configured).
vo.equals(other)booleanStructural 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, undefined values are stripped, and Date/BigInt objects 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

EntityValue Object
IdentityHas id — equality by identityNo id — equality by attributes
MutabilityMutable through actionsMutable through optional actions
Use case”A specific user""An amount of money”
equals()Compares idCompares 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 ≠ Username at compile time.
  • Props are frozen. To “change” a VO, use an action (which mutates a draft copy internally).