agentskills.codes
HA

haskou-value-objects

Use @haskou/value-objects correctly in TypeScript DDD projects. Enforce Value Object behavior, Enum-style domain values, Demeter-friendly comparisons, serialization boundaries, SOLID, cohesive naming, and project naming conventions.

Install

mkdir -p .claude/skills/haskou-value-objects && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/13675" && unzip -o skill.zip -d .claude/skills/haskou-value-objects && rm skill.zip

Installs to .claude/skills/haskou-value-objects

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Use @haskou/value-objects correctly in TypeScript DDD projects. Enforce Value Object behavior, Enum-style domain values, Demeter-friendly comparisons, serialization boundaries, SOLID, cohesive naming, and project naming conventions.
232 charsno explicit “when” trigger

About this skill

@haskou/value-objects Skill

Use this skill whenever touching TypeScript code that creates, compares, serializes, hydrates, validates, or tests Value Objects using @haskou/value-objects.

This project works with DDD. Treat Value Objects as domain behavior, not decorated primitives. The whole point is to stop spraying strings, numbers, enums, and bags of props through the domain like confetti at a legacy wedding.

Core rule

Do not unwrap a Value Object to make a domain decision.

Use the methods exposed by the object:

  • isEqual(other) and isNotEqual(other) for equality.
  • Domain-specific predicates like isPaid(), isDraft(), canBeCancelled(), belongsToCustomer(customerId), etc. when the comparison has business meaning.
  • isGreaterThan, isGreaterOrEqualThan, isLessThan, isLessOrEqualThan, isZero for numeric Value Objects.
  • add, subtract, multiply, divide for numeric operations.
  • isBefore, isAfter, isBeforeOrEqual, isAfterOrEqual, isSameDay, isSameMonth, isSameYear for timestamps.
  • includes, getOverlappingInterval, getDuration, getStart, getEnd for timestamp intervals.
  • Specific accessors like getLatitude, getLongitude, getMonth, getYear, getDay, getHours, getMinutes, etc.

Bad:

const isSameUser = user.id.toPrimitives() === otherUser.id.toPrimitives();
const isSameEmail = user.email.valueOf() === email.valueOf();
const startsBefore = interval.toPrimitives().start < timestamp.valueOf();
const isPaid = order.status.valueOf() === 'paid';

Good:

const isSameUser = user.id.isEqual(otherUser.id);
const isSameEmail = user.email.isEqual(email);
const includesTimestamp = interval.includes(timestamp);
const isPaid = order.status.isPaid();

Boundary rule

Primitives live at boundaries. Value Objects live in the domain.

Application messages, HTTP DTOs, CLI inputs, event payloads, persistence rows, and OpenAPI schemas receive or expose primitives. Application services convert primitives into Value Objects before calling domain constructors or domain methods.

Domain constructors must receive Value Objects, not primitive props bags.

Bad:

new User({
  id: '507f1f77bcf86cd799439011',
  email: '[email protected]',
});

Good:

new User(new UserId('507f1f77bcf86cd799439011'), new Email('[email protected]'));

Serialization and hydration

toPrimitives() and fromPrimitives() are for crossing boundaries only:

  • persistence mappers,
  • DTO mapping,
  • published events,
  • OpenAPI/API responses,
  • test snapshots of serialized contracts.

Never use toPrimitives() for equality, ordering, branching, filtering, or deciding business rules. That is a Demeter violation and a fast lane back to primitive obsession, which humanity apparently keeps reinventing for sport.

valueOf() and toString() are also boundary tools. Use them when writing primitives out to persistence, logs, telemetry, API responses, or external libraries. Do not use them as the default comparison mechanism inside domain or application code.

For Enum-style Value Objects, expose fromPrimitives(value) or another explicit boundary factory when hydration needs to accept a raw string or number. Keep invalid-value rejection inside the Value Object constructor/factory, not scattered through controllers, repositories, handlers, or other procedural junk drawers.

Preferred imports

Import from the actual package name unless the project already has a local wrapper or path alias:

import {
  Email,
  Enum,
  PositiveNumber,
  ShortId,
  Timestamp,
} from '@haskou/value-objects';

Follow existing project import conventions first. Do not invent a parallel Value Object abstraction when the package already covers the case.

If the codebase has a local EnumValueObject wrapper or alias, treat it as an Enum-style Value Object and apply the same rules. Do not introduce both Enum and EnumValueObject patterns in the same bounded context unless the existing codebase already made that mess and you are containing it, not feeding it.

Built-in Value Objects and methods to prefer

Common primitives:

  • ValueObject<T>: base class for single primitive Value Objects.
  • StringValueObject: string validation and isEmpty().
  • NumberValueObject: numeric comparisons and arithmetic.
  • Integer: whole numbers.
  • PositiveNumber: numbers greater than zero.
  • Email: validated email values.
  • Color: validated hex colors, predefined colors, and case-insensitive isEqual.

Finite domain values:

  • Enum<T>: base class for finite, validated primitive sets such as statuses, types, modes, roles, categories, frequencies, or provider codes.
  • Use getValues() to declare the allowed primitive values.
  • Put predicates and transitions on the concrete class, not in random switch statements.

Identifiers:

  • ShortId.generate() for MongoDB ObjectId-style ids.
  • UUID.generate() for UUID v4 ids.
  • Compare ids with isEqual, never by string extraction.

Time:

  • Timestamp.now(), Timestamp.new(value), Timestamp.fromSeconds(value).
  • Timestamp comparison and arithmetic methods instead of primitive math.
  • CalendarDay, Day, DayOfWeek, Month, MonthOfYear, Year, Hour, Duration for explicit temporal concepts.
  • TimestampInterval.fromPrimitives() and toPrimitives() only for serialization/hydration.

Coordinates:

  • Latitude, Longitude, Coordinates.
  • Use Coordinates.fromString(value) when parsing external strings.
  • Use getLatitude() and getLongitude() when domain behavior needs the coordinate parts.

Collections:

  • UniqueObjectArray<T> expects items with isEqual(item).
  • Use it for uniqueness by Value Object behavior instead of deduping with primitives.
  • Prefer includes, push, remove, length, and toArray() over hand-rolled array rituals.

Hashes, media, and crypto:

  • Use MD5Hash, SHA256Hash, SHA512Hash, Media, KeyPair, EncryptedKeyPair, PrivateKey, EncryptedPrivateKey, PublicKey, Signature, etc. when the domain actually needs those concepts.
  • Use Hash.from(...), toBase64(), Media.getBuffer(), Media.getSize(), and Media.getBase64() at boundaries or crypto/media behavior points.
  • Do not leak crypto payload internals across the domain. Keep encryption/signing behavior on the relevant objects.

Nullish behavior:

  • ValueObject supports the library's Null Object behavior for null or undefined inputs.
  • Do not pass null or undefined intentionally to mean a business state unless the existing design explicitly models that pattern.
  • Prefer explicit domain concepts such as OptionalDeliveryDate, UnassignedOwner, or nullable fields at the boundary when absence is part of the language.

Enum / EnumValueObject-style Value Objects

Use an Enum-style Value Object when the domain concept has a closed set of valid primitive values and may grow behavior over time.

Good candidates:

  • OrderStatus, PaymentStatus, InvoiceStatus.
  • CurrencyCode, CountryCode, LocaleCode when the domain cares about the allowed set.
  • NotificationChannel, ShipmentProvider, BillingPeriod, UserRole.
  • PlanType, FeatureFlag, PermissionScope, RetryStrategy.

Bad candidates:

  • Free text fields.
  • Unbounded external provider values that are not controlled by the domain.
  • UI labels, translated strings, icons, colors, CSS classes, or display names. That stuff belongs in presenters/mappers, because apparently even strings need costumes now.

The package exports Enum. Some projects may call the same idea EnumValueObject. The rule is the same: the class owns valid values, comparison, predicates, and domain behavior.

Bad:

export enum OrderStatus {
  DRAFT = 'draft',
  PAID = 'paid',
  CANCELLED = 'cancelled',
}

if (order.status === OrderStatus.PAID) {
  // domain decision outside the domain model
}

if (order.status === 'cancelled') {
  // raw string cosplay
}

Good:

import { Enum } from '@haskou/value-objects';

enum OrderStatusPrimitive {
  DRAFT = 'draft',
  PAID = 'paid',
  CANCELLED = 'cancelled',
  SHIPPED = 'shipped',
}

export class OrderStatus extends Enum<string> {
  static readonly DRAFT = new OrderStatus(OrderStatusPrimitive.DRAFT);
  static readonly PAID = new OrderStatus(OrderStatusPrimitive.PAID);
  static readonly CANCELLED = new OrderStatus(OrderStatusPrimitive.CANCELLED);
  static readonly SHIPPED = new OrderStatus(OrderStatusPrimitive.SHIPPED);

  private constructor(value: string) {
    super(value);
  }

  static fromPrimitives(value: string): OrderStatus {
    return new OrderStatus(value);
  }

  getValues(): string[] {
    return Object.values(OrderStatusPrimitive);
  }

  isDraft(): boolean {
    return this.isEqual(OrderStatus.DRAFT);
  }

  isPaid(): boolean {
    return this.isEqual(OrderStatus.PAID);
  }

  isCancelled(): boolean {
    return this.isEqual(OrderStatus.CANCELLED);
  }

  canBePaid(): boolean {
    return this.isDraft();
  }

  canBeShipped(): boolean {
    return this.isPaid();
  }

  pay(): OrderStatus {
    if (this.canBePaid() === false) {
      throw new Error('Only draft orders can be paid');
    }

    return OrderStatus.PAID;
  }
}

Rules:

  • Keep the primitive enum, literal list, or allowed-values object private to the Value Object module unless the existing codebase already exposes it as contract.
  • Prefer static readonly instances for named values.
  • Provide fromPrimitives(value) when external layers hydrate from strings/numbers.
  • Do not export raw enum primitives as the domain API.
  • Do not compare status.valueOf() to strings inside entities, aggregates, application services, policies, or tests.
  • Do not switch on status.valueOf() in domain code. Add behavior to the Value Object or to the aggregate.
  • Use semantic methods: isPaid, isFinal, canTransitionTo, canBeCancelled, requiresInvoice, etc.
  • Keep transition rules close to the aggregate when they require aggregate s

Content truncated.

Search skills

Search the agent skills registry