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.zipInstalls 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.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)andisNotEqual(other)for equality.- Domain-specific predicates like
isPaid(),isDraft(),canBeCancelled(),belongsToCustomer(customerId), etc. when the comparison has business meaning. isGreaterThan,isGreaterOrEqualThan,isLessThan,isLessOrEqualThan,isZerofor numeric Value Objects.add,subtract,multiply,dividefor numeric operations.isBefore,isAfter,isBeforeOrEqual,isAfterOrEqual,isSameDay,isSameMonth,isSameYearfor timestamps.includes,getOverlappingInterval,getDuration,getStart,getEndfor 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 andisEmpty().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-insensitiveisEqual.
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
switchstatements.
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,Durationfor explicit temporal concepts.TimestampInterval.fromPrimitives()andtoPrimitives()only for serialization/hydration.
Coordinates:
Latitude,Longitude,Coordinates.- Use
Coordinates.fromString(value)when parsing external strings. - Use
getLatitude()andgetLongitude()when domain behavior needs the coordinate parts.
Collections:
UniqueObjectArray<T>expects items withisEqual(item).- Use it for uniqueness by Value Object behavior instead of deduping with primitives.
- Prefer
includes,push,remove,length, andtoArray()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(), andMedia.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:
ValueObjectsupports the library's Null Object behavior fornullorundefinedinputs.- Do not pass
nullorundefinedintentionally 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,LocaleCodewhen 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.