@vytches/ddd-value-objects 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +76 -1333
  2. package/package.json +6 -6
package/README.md CHANGED
@@ -1,1391 +1,134 @@
1
1
  # @vytches/ddd-value-objects
2
2
 
3
- <!-- LLM-METADATA
4
- Package: @vytches/ddd-value-objects
5
- Category: Foundation
6
- Purpose: Common value object implementations including EntityId, Money, Email, and other domain value objects
7
- Dependencies: @vytches/ddd-domain-primitives, @vytches/ddd-contracts
8
- Complexity: Medium
9
- DDD Patterns: Value Object, Entity Identifier, Domain Primitive
10
- Integration Points: Used by all domain models and aggregates
11
- -->
12
-
13
3
  [![npm version](https://badge.fury.io/js/%40vytches%2Fddd-value-objects.svg)](https://badge.fury.io/js/%40vytches%2Fddd-value-objects)
14
4
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?logo=typescript)](https://www.typescriptlang.org/)
15
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16
6
 
17
- > **Common value object implementations for Domain-Driven Design**
18
-
19
- Collection of battle-tested value objects including EntityId, Money, Email,
20
- Address, and other common domain primitives. Built with validation,
21
- immutability, and rich behavior patterns.
7
+ > **Base value object class, EntityId, and branded ID types**
22
8
 
23
- ## 📋 Table of Contents
9
+ Provides the foundation for value objects in DDD — immutability, structural
10
+ equality, and type-safe entity identifiers.
24
11
 
25
- - [Installation](#installation)
26
- - [Key Features](#key-features)
27
- - [Core Concepts](#core-concepts)
28
- - [Quick Start](#quick-start)
29
- - [EntityId](#entityid)
30
- - [Money](#money)
31
- - [Email](#email)
32
- - [Address](#address)
33
- - [PhoneNumber](#phonenumber)
34
- - [DateRange](#daterange)
35
- - [Custom Value Objects](#custom-value-objects)
36
- - [Validation](#validation)
37
- - [Serialization](#serialization)
38
- - [Testing](#testing)
39
- - [Best Practices](#best-practices)
40
- - [Contributing](#contributing)
41
-
42
- ## 🚀 Installation
12
+ ## Installation
43
13
 
44
14
  ```bash
45
- # npm
46
- npm install @vytches/ddd-value-objects
47
-
48
- # yarn
49
- yarn add @vytches/ddd-value-objects
50
-
51
- # pnpm
52
15
  pnpm add @vytches/ddd-value-objects
53
16
  ```
54
17
 
55
- ### Peer Dependencies
56
-
57
- ```bash
58
- # Required for full functionality
59
- npm install @vytches/ddd-domain-primitives @vytches/ddd-contracts
60
- ```
61
-
62
- ## ✨ Key Features
63
-
64
- ### Rich Value Objects
65
-
66
- - **EntityId**: Strongly-typed entity identifiers with multiple formats
67
- - **Money**: Currency-aware monetary values with arithmetic operations
68
- - **Email**: Validated email addresses with domain extraction
69
- - **Address**: Structured address representation with validation
70
-
71
- ### Advanced Features
72
-
73
- - **Immutability**: All value objects are immutable by design
74
- - **Validation**: Built-in validation with meaningful error messages
75
- - **Serialization**: JSON serialization and deserialization support
76
- - **Comparison**: Proper equality comparison and hash code generation
77
-
78
- ### Developer Experience
79
-
80
- - **Type Safety**: Full TypeScript support with strict typing
81
- - **Factory Methods**: Convenient creation methods for common patterns
82
- - **Rich Behavior**: Domain-specific methods and operations
83
- - **Testing Support**: Comprehensive testing utilities and builders
84
-
85
- ## 🎯 Core Concepts
86
-
87
- ### Value Object Pattern
88
-
89
- Value objects are immutable objects that are defined by their value rather than
90
- their identity:
91
-
92
- ```typescript
93
- // Value objects are compared by value, not reference
94
- const money1 = new Money(100, 'USD');
95
- const money2 = new Money(100, 'USD');
96
-
97
- console.log(money1.equals(money2)); // true
98
- console.log(money1 === money2); // false (different instances)
99
-
100
- // Value objects are immutable
101
- const money3 = money1.add(new Money(50, 'USD'));
102
- console.log(money1.amount); // 100 (unchanged)
103
- console.log(money3.amount); // 150 (new instance)
104
- ```
105
-
106
- ### Entity Identification
107
-
108
- EntityId provides strongly-typed identifiers for domain entities:
109
-
110
- ```typescript
111
- // Type-safe entity identification
112
- const userId = EntityId.create(); // UUID by default
113
- const orderId = EntityId.fromString('order-123');
114
- const productId = EntityId.fromInteger(456);
115
-
116
- // Prevents mixing entity types
117
- function processOrder(orderId: EntityId, userId: EntityId) {
118
- // Type safety prevents mistakes
119
- }
120
-
121
- processOrder(orderId, userId); // ✅ Correct
122
- processOrder(userId, orderId); // ❌ Type error
123
- ```
124
-
125
- ## 🚀 Quick Start
126
-
127
- ### 1. Using EntityId
128
-
129
- ```typescript
130
- import { EntityId } from '@vytches/ddd-value-objects';
131
-
132
- // Create new UUID-based ID
133
- const id = EntityId.create();
134
- console.log(id.value); // "550e8400-e29b-41d4-a716-446655440000"
135
-
136
- // Create from string
137
- const customId = EntityId.fromString('user-123');
138
- console.log(customId.value); // "user-123"
139
-
140
- // Create from integer
141
- const numericId = EntityId.fromInteger(42);
142
- console.log(numericId.value); // "42"
143
-
144
- // Create from big integer
145
- const bigId = EntityId.fromBigInt(BigInt('123456789012345'));
146
- console.log(bigId.value); // "123456789012345"
147
-
148
- // Type-safe comparison
149
- const id1 = EntityId.create();
150
- const id2 = EntityId.create();
151
- console.log(id1.equals(id2)); // false
152
- console.log(id1.equals(id1)); // true
153
- ```
154
-
155
- ### 2. Using Money
156
-
157
- ```typescript
158
- import { Money } from '@vytches/ddd-value-objects';
159
-
160
- // Create money values
161
- const price = new Money(99.99, 'USD');
162
- const tax = new Money(8.5, 'USD');
163
-
164
- // Arithmetic operations
165
- const total = price.add(tax);
166
- console.log(total.amount); // 108.49
167
- console.log(total.currency); // 'USD'
168
-
169
- // Comparison operations
170
- const budget = new Money(100, 'USD');
171
- console.log(total.isGreaterThan(budget)); // true
172
- console.log(total.isLessThan(budget)); // false
173
-
174
- // Formatting
175
- console.log(total.format()); // "$108.49"
176
- console.log(total.format('en-EU')); // "108,49 $"
177
- ```
178
-
179
- ### 3. Using Email
180
-
181
- ```typescript
182
- import { Email } from '@vytches/ddd-value-objects';
183
-
184
- // Create validated email
185
- const email = new Email('user@example.com');
186
- console.log(email.value); // "user@example.com"
187
-
188
- // Extract parts
189
- console.log(email.localPart); // "user"
190
- console.log(email.domain); // "example.com"
191
- console.log(email.topLevelDomain); // "com"
192
-
193
- // Validation
194
- try {
195
- const invalidEmail = new Email('invalid-email');
196
- } catch (error) {
197
- console.log(error.message); // "Invalid email format"
198
- }
199
-
200
- // Domain operations
201
- console.log(email.isDomainEmail('example.com')); // true
202
- console.log(email.isPersonalEmail()); // false (not gmail, yahoo, etc.)
203
- ```
204
-
205
- ### 4. Using Address
206
-
207
- ```typescript
208
- import { Address } from '@vytches/ddd-value-objects';
209
-
210
- // Create address
211
- const address = new Address('123 Main St', 'Anytown', 'NY', '12345', 'USA');
212
-
213
- // Access components
214
- console.log(address.street); // "123 Main St"
215
- console.log(address.city); // "Anytown"
216
- console.log(address.state); // "NY"
217
- console.log(address.zipCode); // "12345"
218
- console.log(address.country); // "USA"
219
-
220
- // Formatted output
221
- console.log(address.getFullAddress());
222
- // "123 Main St, Anytown, NY 12345, USA"
223
-
224
- // Validation
225
- try {
226
- const invalidAddress = new Address('', 'City', 'State', 'ZIP', 'Country');
227
- } catch (error) {
228
- console.log(error.message); // "Street address is required"
229
- }
230
- ```
231
-
232
- ## 🆔 EntityId
233
-
234
- ### Creation Methods
235
-
236
- ```typescript
237
- import { EntityId } from '@vytches/ddd-value-objects';
238
-
239
- // UUID-based (default)
240
- const uuidId = EntityId.create();
241
- console.log(uuidId.value); // "550e8400-e29b-41d4-a716-446655440000"
242
-
243
- // String-based
244
- const stringId = EntityId.fromString('user-123');
245
- console.log(stringId.value); // "user-123"
246
-
247
- // Integer-based
248
- const intId = EntityId.fromInteger(42);
249
- console.log(intId.value); // "42"
250
-
251
- // Big integer-based
252
- const bigIntId = EntityId.fromBigInt(BigInt('123456789012345'));
253
- console.log(bigIntId.value); // "123456789012345"
254
-
255
- // From existing value (type-safe)
256
- const copyId = EntityId.fromValue(uuidId.value);
257
- console.log(copyId.equals(uuidId)); // true
258
- ```
259
-
260
- ### Validation and Operations
261
-
262
- ```typescript
263
- // Validation
264
- console.log(EntityId.isValid('550e8400-e29b-41d4-a716-446655440000')); // true
265
- console.log(EntityId.isValid('')); // false
266
- console.log(EntityId.isValid(null)); // false
267
-
268
- // Comparison
269
- const id1 = EntityId.create();
270
- const id2 = EntityId.create();
271
- const id3 = EntityId.fromValue(id1.value);
272
-
273
- console.log(id1.equals(id2)); // false
274
- console.log(id1.equals(id3)); // true
275
-
276
- // Serialization
277
- const serialized = id1.toJSON();
278
- console.log(serialized); // { value: "550e8400-e29b-41d4-a716-446655440000" }
279
-
280
- const deserialized = EntityId.fromJSON(serialized);
281
- console.log(deserialized.equals(id1)); // true
282
- ```
283
-
284
- ### Type Safety
285
-
286
- ```typescript
287
- // EntityId provides type safety for entity identification
288
- class User {
289
- constructor(public readonly id: EntityId) {}
290
- }
291
-
292
- class Order {
293
- constructor(
294
- public readonly id: EntityId,
295
- public readonly userId: EntityId
296
- ) {}
297
- }
298
-
299
- // Usage
300
- const userId = EntityId.create();
301
- const orderId = EntityId.create();
302
-
303
- const user = new User(userId);
304
- const order = new Order(orderId, userId);
305
-
306
- // Type safety prevents mistakes
307
- function findUser(id: EntityId): User | null {
308
- // Implementation
309
- return null;
310
- }
311
-
312
- findUser(userId); // ✅ Correct
313
- findUser(orderId); // ✅ Also correct (both are EntityId)
314
- findUser('string-id'); // ❌ Type error
315
- ```
316
-
317
- ## 💰 Money
318
-
319
- ### Creation and Basic Operations
320
-
321
- ```typescript
322
- import { Money } from '@vytches/ddd-value-objects';
323
-
324
- // Create money values
325
- const price = new Money(99.99, 'USD');
326
- const discount = new Money(10.0, 'USD');
327
-
328
- // Arithmetic operations
329
- const discountedPrice = price.subtract(discount);
330
- console.log(discountedPrice.amount); // 89.99
331
-
332
- const doublePrice = price.multiply(2);
333
- console.log(doublePrice.amount); // 199.98
334
-
335
- const halfPrice = price.divide(2);
336
- console.log(halfPrice.amount); // 49.995
337
-
338
- // Rounding
339
- const roundedHalf = price.divide(2).round();
340
- console.log(roundedHalf.amount); // 50.00
341
- ```
342
-
343
- ### Currency Operations
18
+ ## What's included
344
19
 
345
- ```typescript
346
- // Currency validation
347
- try {
348
- const invalidMoney = new Money(100, 'INVALID');
349
- } catch (error) {
350
- console.log(error.message); // "Invalid currency code"
351
- }
20
+ | Export | Kind | Description |
21
+ | ------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
22
+ | `BaseValueObject<T>` | class | Abstract base for value objects; deep-freezes internal value; implements `equals()` via structural comparison |
23
+ | `ValueObjectValidator<T>` | interface | `validate(value: T): boolean` — implement to add invariant checking |
24
+ | `EntityId<T>` | class | Enhanced entity identifier extending the contracts `EntityId`; adds UUID/integer/text validation, `create()`, `fromString()`, `fromNumber()` |
25
+ | `EntityIdFactory` | class | **Deprecated** — will be removed in v1.0.0; use `EntityId.create()` directly |
26
+ | `BrandedId<Tag>` | type | Compile-time branded `EntityId<string>` — prevents mixing IDs across aggregate types |
27
+ | `createBrandedId<Tag>(id)` | function | Casts an `EntityId` to `BrandedId<Tag>` |
28
+ | `newBrandedId<Tag>()` | function | Creates a new UUID-based `BrandedId<Tag>` |
29
+ | `brandedIdFromUUID<Tag>(uuid)` | function | Creates a `BrandedId<Tag>` from an existing UUID string |
30
+ | `brandedIdFromText<Tag>(text)` | function | Creates a `BrandedId<Tag>` from a text string |
31
+ | `IEntityId<T>` | interface | Re-exported from `@vytches/ddd-contracts` |
32
+ | `IEntityIdFactory` | interface | Re-exported from `@vytches/ddd-contracts` |
33
+ | `IdType` | type | Re-exported from `@vytches/ddd-contracts` — `'uuid' \| 'integer' \| 'text'` |
352
34
 
353
- // Currency compatibility
354
- const usd = new Money(100, 'USD');
355
- const eur = new Money(85, 'EUR');
35
+ ## Note on bundled domain examples
356
36
 
357
- try {
358
- const mixed = usd.add(eur);
359
- } catch (error) {
360
- console.log(error.message); // "Cannot operate on different currencies"
361
- }
37
+ This package does **not** export `Email`, `Money`, `Address`, `PhoneNumber`,
38
+ `DateRange`, or other concrete domain value objects. Those are application-level
39
+ types — implement them in your own domain layer using `BaseValueObject` as the
40
+ base class.
362
41
 
363
- // Currency conversion (with external rate)
364
- const conversionRate = 0.85;
365
- const convertedToEur = usd.convertTo('EUR', conversionRate);
366
- console.log(convertedToEur.amount); // 85.00
367
- console.log(convertedToEur.currency); // "EUR"
368
- ```
42
+ ## Quick start
369
43
 
370
- ### Comparison and Validation
44
+ ### Custom value object
371
45
 
372
46
  ```typescript
373
- const money1 = new Money(100, 'USD');
374
- const money2 = new Money(200, 'USD');
375
-
376
- // Comparison
377
- console.log(money1.isLessThan(money2)); // true
378
- console.log(money1.isGreaterThan(money2)); // false
379
- console.log(money1.equals(money2)); // false
380
-
381
- // Validation
382
- console.log(money1.isPositive()); // true
383
- console.log(money1.isNegative()); // false
384
- console.log(money1.isZero()); // false
385
-
386
- // Zero and negative values
387
- const zero = Money.zero('USD');
388
- console.log(zero.isZero()); // true
389
-
390
- const negative = new Money(-50, 'USD');
391
- console.log(negative.isNegative()); // true
392
- ```
393
-
394
- ### Formatting
395
-
396
- ```typescript
397
- const money = new Money(1234.56, 'USD');
398
-
399
- // Default formatting
400
- console.log(money.format()); // "$1,234.56"
401
-
402
- // Locale-specific formatting
403
- console.log(money.format('en-US')); // "$1,234.56"
404
- console.log(money.format('en-GB')); // "US$1,234.56"
405
- console.log(money.format('de-DE')); // "1.234,56 $"
406
-
407
- // Custom formatting
408
- console.log(
409
- money.format('en-US', {
410
- minimumFractionDigits: 3,
411
- maximumFractionDigits: 3,
412
- })
413
- ); // "$1,234.560"
414
- ```
415
-
416
- ## 📧 Email
417
-
418
- ### Creation and Validation
419
-
420
- ```typescript
421
- import { Email } from '@vytches/ddd-value-objects';
422
-
423
- // Valid email creation
424
- const email = new Email('user@example.com');
425
- console.log(email.value); // "user@example.com"
426
-
427
- // Case normalization
428
- const upperEmail = new Email('USER@EXAMPLE.COM');
429
- console.log(upperEmail.value); // "user@example.com"
430
-
431
- // Validation errors
432
- try {
433
- new Email('invalid-email');
434
- } catch (error) {
435
- console.log(error.message); // "Invalid email format"
436
- }
47
+ import { BaseValueObject } from '@vytches/ddd-value-objects';
437
48
 
438
- try {
439
- new Email('');
440
- } catch (error) {
441
- console.log(error.message); // "Email cannot be empty"
49
+ interface MoneyProps {
50
+ amount: number;
51
+ currency: string;
442
52
  }
443
- ```
444
-
445
- ### Email Analysis
446
-
447
- ```typescript
448
- const email = new Email('john.doe+newsletter@company.co.uk');
449
-
450
- // Component extraction
451
- console.log(email.localPart); // "john.doe+newsletter"
452
- console.log(email.domain); // "company.co.uk"
453
- console.log(email.topLevelDomain); // "uk"
454
-
455
- // Advanced analysis
456
- console.log(email.hasPlus()); // true
457
- console.log(email.getPlusTag()); // "newsletter"
458
- console.log(email.getBaseEmail()); // "john.doe@company.co.uk"
459
-
460
- // Domain analysis
461
- console.log(email.isDomainEmail('company.co.uk')); // true
462
- console.log(email.isPersonalEmail()); // false
463
- console.log(email.isBusinessEmail()); // true
464
- ```
465
-
466
- ### Common Email Patterns
467
-
468
- ```typescript
469
- // Personal email detection
470
- const personalEmails = [
471
- 'user@gmail.com',
472
- 'user@yahoo.com',
473
- 'user@hotmail.com',
474
- 'user@outlook.com',
475
- ];
476
-
477
- personalEmails.forEach(emailStr => {
478
- const email = new Email(emailStr);
479
- console.log(`${emailStr}: ${email.isPersonalEmail()}`); // true for all
480
- });
481
-
482
- // Business email detection
483
- const businessEmail = new Email('employee@company.com');
484
- console.log(businessEmail.isBusinessEmail()); // true
485
53
 
486
- // Disposable email detection
487
- const disposableEmail = new Email('user@10minutemail.com');
488
- console.log(disposableEmail.isDisposableEmail()); // true
489
- ```
490
-
491
- ## 🏠 Address
492
-
493
- ### Creation and Components
494
-
495
- ```typescript
496
- import { Address } from '@vytches/ddd-value-objects';
497
-
498
- // Complete address
499
- const address = new Address(
500
- '123 Main Street, Apt 4B',
501
- 'New York',
502
- 'NY',
503
- '10001',
504
- 'USA'
505
- );
506
-
507
- // Access components
508
- console.log(address.street); // "123 Main Street, Apt 4B"
509
- console.log(address.city); // "New York"
510
- console.log(address.state); // "NY"
511
- console.log(address.zipCode); // "10001"
512
- console.log(address.country); // "USA"
513
-
514
- // Extended address (with optional fields)
515
- const extendedAddress = new Address(
516
- '456 Oak Avenue',
517
- 'Los Angeles',
518
- 'CA',
519
- '90210',
520
- 'USA',
521
- 'Suite 100' // optional apartment/unit
522
- );
523
-
524
- console.log(extendedAddress.apartment); // "Suite 100"
525
- ```
526
-
527
- ### Address Formatting
528
-
529
- ```typescript
530
- const address = new Address('123 Main St', 'Anytown', 'NY', '12345', 'USA');
531
-
532
- // Full address formatting
533
- console.log(address.getFullAddress());
534
- // "123 Main St, Anytown, NY 12345, USA"
535
-
536
- // Mailing format
537
- console.log(address.getMailingFormat());
538
- // "123 Main St\nAnytown, NY 12345\nUSA"
539
-
540
- // Short format (without country)
541
- console.log(address.getShortFormat());
542
- // "123 Main St, Anytown, NY 12345"
543
- ```
544
-
545
- ### Address Validation
546
-
547
- ```typescript
548
- // Required field validation
549
- try {
550
- new Address('', 'City', 'State', 'ZIP', 'Country');
551
- } catch (error) {
552
- console.log(error.message); // "Street address is required"
553
- }
554
-
555
- // ZIP code validation
556
- try {
557
- new Address('123 Main St', 'City', 'NY', '', 'USA');
558
- } catch (error) {
559
- console.log(error.message); // "ZIP code is required"
560
- }
561
-
562
- // Country-specific validation
563
- const usAddress = new Address('123 Main St', 'City', 'NY', '12345', 'USA');
564
- console.log(usAddress.isUSAddress()); // true
565
- console.log(usAddress.isValidUSZipCode()); // true
566
-
567
- const ukAddress = new Address('123 High St', 'London', 'ENG', 'SW1A 1AA', 'UK');
568
- console.log(ukAddress.isUKAddress()); // true
569
- console.log(ukAddress.isValidUKPostcode()); // true
570
- ```
571
-
572
- ## 📞 PhoneNumber
573
-
574
- ### Creation and Validation
575
-
576
- ```typescript
577
- import { PhoneNumber } from '@vytches/ddd-value-objects';
578
-
579
- // US phone number
580
- const phoneNumber = new PhoneNumber('+1 (555) 123-4567');
581
- console.log(phoneNumber.value); // "+15551234567"
582
-
583
- // International number
584
- const intlNumber = new PhoneNumber('+44 20 7946 0958');
585
- console.log(intlNumber.value); // "+442079460958"
586
-
587
- // Validation
588
- try {
589
- new PhoneNumber('invalid');
590
- } catch (error) {
591
- console.log(error.message); // "Invalid phone number format"
592
- }
593
- ```
594
-
595
- ### Number Analysis
596
-
597
- ```typescript
598
- const phoneNumber = new PhoneNumber('+1 (555) 123-4567');
599
-
600
- // Component extraction
601
- console.log(phoneNumber.getCountryCode()); // "1"
602
- console.log(phoneNumber.getAreaCode()); // "555"
603
- console.log(phoneNumber.getNumber()); // "1234567"
604
-
605
- // Type detection
606
- console.log(phoneNumber.isUSNumber()); // true
607
- console.log(phoneNumber.isMobileNumber()); // false (555 is not mobile)
608
- console.log(phoneNumber.isTollFree()); // false
609
-
610
- // Formatting
611
- console.log(phoneNumber.format()); // "+1 (555) 123-4567"
612
- console.log(phoneNumber.formatNational()); // "(555) 123-4567"
613
- console.log(phoneNumber.formatInternational()); // "+1 555 123 4567"
614
- ```
615
-
616
- ### Mobile and Special Numbers
617
-
618
- ```typescript
619
- // Mobile number
620
- const mobileNumber = new PhoneNumber('+1 (555) 000-1234');
621
- console.log(mobileNumber.isMobileNumber()); // true
622
-
623
- // Toll-free number
624
- const tollFreeNumber = new PhoneNumber('+1 (800) 123-4567');
625
- console.log(tollFreeNumber.isTollFree()); // true
626
-
627
- // International mobile
628
- const intlMobile = new PhoneNumber('+44 7700 900123');
629
- console.log(intlMobile.isMobileNumber()); // true
630
- console.log(intlMobile.getCountryCode()); // "44"
631
- ```
632
-
633
- ## 📅 DateRange
634
-
635
- ### Creation and Validation
636
-
637
- ```typescript
638
- import { DateRange } from '@vytches/ddd-value-objects';
639
-
640
- // Create date range
641
- const startDate = new Date('2023-01-01');
642
- const endDate = new Date('2023-12-31');
643
- const yearRange = new DateRange(startDate, endDate);
644
-
645
- console.log(yearRange.startDate); // 2023-01-01T00:00:00.000Z
646
- console.log(yearRange.endDate); // 2023-12-31T00:00:00.000Z
647
-
648
- // Validation
649
- try {
650
- new DateRange(endDate, startDate); // End before start
651
- } catch (error) {
652
- console.log(error.message); // "End date must be after start date"
653
- }
654
- ```
655
-
656
- ### Range Operations
657
-
658
- ```typescript
659
- const range1 = new DateRange(new Date('2023-01-01'), new Date('2023-06-30'));
660
-
661
- const range2 = new DateRange(new Date('2023-04-01'), new Date('2023-09-30'));
662
-
663
- // Overlap detection
664
- console.log(range1.overlaps(range2)); // true
665
-
666
- // Contains date
667
- const testDate = new Date('2023-03-15');
668
- console.log(range1.contains(testDate)); // true
669
-
670
- // Duration calculation
671
- console.log(range1.getDurationInDays()); // 180
672
- console.log(range1.getDurationInMonths()); // 6
673
-
674
- // Range intersection
675
- const intersection = range1.getIntersection(range2);
676
- console.log(intersection.startDate); // 2023-04-01
677
- console.log(intersection.endDate); // 2023-06-30
678
- ```
679
-
680
- ### Date Range Utilities
681
-
682
- ```typescript
683
- // Business days calculation
684
- const businessDays = range1.getBusinessDays();
685
- console.log(businessDays); // Excludes weekends
686
-
687
- // Split into periods
688
- const monthlyPeriods = range1.splitIntoMonths();
689
- console.log(monthlyPeriods.length); // 6
690
-
691
- // Extend range
692
- const extendedRange = range1.extend(30); // Extend by 30 days
693
- console.log(extendedRange.endDate); // 2023-07-30
694
-
695
- // Common ranges
696
- const thisMonth = DateRange.thisMonth();
697
- const thisYear = DateRange.thisYear();
698
- const lastWeek = DateRange.lastWeek();
699
- ```
700
-
701
- ## 🏗️ Custom Value Objects
702
-
703
- ### Creating Custom Value Objects
704
-
705
- ```typescript
706
- import { ValueObject } from '@vytches/ddd-domain-primitives';
707
-
708
- // Custom value object for Social Security Number
709
- class SocialSecurityNumber extends ValueObject {
710
- private readonly value: string;
711
-
712
- constructor(ssn: string) {
713
- super();
714
- this.value = this.normalize(ssn);
54
+ class Money extends BaseValueObject<MoneyProps> {
55
+ constructor(amount: number, currency: string) {
56
+ super({ amount, currency });
715
57
  }
716
58
 
717
- public get Value(): string {
718
- return this.value;
59
+ validate(value: MoneyProps): boolean {
60
+ return value.amount >= 0 && value.currency.length === 3;
719
61
  }
720
62
 
721
- public format(): string {
722
- return `${this.value.slice(0, 3)}-${this.value.slice(3, 5)}-${this.value.slice(5)}`;
63
+ get amount(): number {
64
+ return this.value.amount;
723
65
  }
724
-
725
- public getAreaNumber(): string {
726
- return this.value.slice(0, 3);
66
+ get currency(): string {
67
+ return this.value.currency;
727
68
  }
728
69
 
729
- public getGroupNumber(): string {
730
- return this.value.slice(3, 5);
731
- }
732
-
733
- public getSerialNumber(): string {
734
- return this.value.slice(5);
735
- }
736
-
737
- protected getEqualityComponents(): any[] {
738
- return [this.value];
739
- }
740
-
741
- private normalize(ssn: string): string {
742
- // Remove all non-digit characters
743
- const digits = ssn.replace(/\D/g, '');
744
-
745
- if (digits.length !== 9) {
746
- throw new ValidationError('SSN must be 9 digits', 'ssn', ssn);
747
- }
748
-
749
- if (digits === '000000000' || digits === '123456789') {
750
- throw new ValidationError('Invalid SSN pattern', 'ssn', ssn);
751
- }
752
-
753
- return digits;
70
+ add(other: Money): Money {
71
+ if (this.currency !== other.currency) throw new Error('Currency mismatch');
72
+ return new Money(this.amount + other.amount, this.currency);
754
73
  }
755
74
  }
756
75
 
757
- // Usage
758
- const ssn = new SocialSecurityNumber('123-45-6789');
759
- console.log(ssn.Value); // "123456789"
760
- console.log(ssn.format()); // "123-45-6789"
761
- console.log(ssn.getAreaNumber()); // "123"
76
+ const price = new Money(10, 'USD');
77
+ const tax = new Money(1.5, 'USD');
78
+ const total = price.add(tax);
79
+
80
+ console.log(total.equals(new Money(11.5, 'USD'))); // true
762
81
  ```
763
82
 
764
- ### Complex Value Objects
83
+ ### EntityId
765
84
 
766
85
  ```typescript
767
- // Custom value object for geographic coordinates
768
- class Coordinates extends ValueObject {
769
- constructor(
770
- private readonly latitude: number,
771
- private readonly longitude: number
772
- ) {
773
- super();
774
- this.validate();
775
- }
776
-
777
- public get Latitude(): number {
778
- return this.latitude;
779
- }
780
-
781
- public get Longitude(): number {
782
- return this.longitude;
783
- }
784
-
785
- public distanceTo(other: Coordinates): number {
786
- // Haversine formula for distance calculation
787
- const R = 6371; // Earth's radius in kilometers
788
- const dLat = this.toRadians(other.latitude - this.latitude);
789
- const dLon = this.toRadians(other.longitude - this.longitude);
790
-
791
- const a =
792
- Math.sin(dLat / 2) * Math.sin(dLat / 2) +
793
- Math.cos(this.toRadians(this.latitude)) *
794
- Math.cos(this.toRadians(other.latitude)) *
795
- Math.sin(dLon / 2) *
796
- Math.sin(dLon / 2);
797
-
798
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
799
- return R * c;
800
- }
86
+ import { EntityId } from '@vytches/ddd-value-objects';
801
87
 
802
- public format(): string {
803
- return `${this.latitude.toFixed(6)}, ${this.longitude.toFixed(6)}`;
804
- }
88
+ // Generate a new UUID-based ID
89
+ const id = EntityId.create(); // EntityId<string> with UUID
805
90
 
806
- protected getEqualityComponents(): any[] {
807
- return [this.latitude, this.longitude];
808
- }
91
+ // From an existing UUID string
92
+ const fromString = EntityId.fromString('550e8400-e29b-41d4-a716-446655440000');
809
93
 
810
- private validate(): void {
811
- if (this.latitude < -90 || this.latitude > 90) {
812
- throw new ValidationError(
813
- 'Latitude must be between -90 and 90',
814
- 'latitude',
815
- this.latitude
816
- );
817
- }
94
+ // From an integer
95
+ const fromNumber = EntityId.fromNumber(42);
818
96
 
819
- if (this.longitude < -180 || this.longitude > 180) {
820
- throw new ValidationError(
821
- 'Longitude must be between -180 and 180',
822
- 'longitude',
823
- this.longitude
824
- );
825
- }
826
- }
827
-
828
- private toRadians(degrees: number): number {
829
- return degrees * (Math.PI / 180);
830
- }
831
- }
832
-
833
- // Usage
834
- const newYork = new Coordinates(40.7128, -74.006);
835
- const london = new Coordinates(51.5074, -0.1278);
836
-
837
- console.log(newYork.format()); // "40.712800, -74.006000"
838
- console.log(newYork.distanceTo(london)); // 5585.27 km
97
+ console.log(id.getValue()); // '550e8400-...'
98
+ console.log(id.equals(EntityId.fromString(id.getValue()))); // true
839
99
  ```
840
100
 
841
- ## ✅ Validation
842
-
843
- ### Built-in Validators
101
+ ### Branded IDs
844
102
 
845
103
  ```typescript
846
104
  import {
847
- EmailValidator,
848
- PhoneNumberValidator,
849
- CurrencyValidator,
850
- ZipCodeValidator,
105
+ BrandedId,
106
+ newBrandedId,
107
+ brandedIdFromUUID,
851
108
  } from '@vytches/ddd-value-objects';
852
109
 
853
- // Email validation
854
- const emailValidator = new EmailValidator();
855
- console.log(emailValidator.isValid('user@example.com')); // true
856
- console.log(emailValidator.isValid('invalid')); // false
857
-
858
- // Phone number validation
859
- const phoneValidator = new PhoneNumberValidator();
860
- console.log(phoneValidator.isValid('+1 (555) 123-4567')); // true
861
- console.log(phoneValidator.isValid('invalid')); // false
862
-
863
- // Currency validation
864
- const currencyValidator = new CurrencyValidator();
865
- console.log(currencyValidator.isValid('USD')); // true
866
- console.log(currencyValidator.isValid('INVALID')); // false
867
-
868
- // ZIP code validation
869
- const zipValidator = new ZipCodeValidator();
870
- console.log(zipValidator.isValidUSZip('12345')); // true
871
- console.log(zipValidator.isValidUSZip('123')); // false
872
- ```
110
+ type OrderId = BrandedId<'Order'>;
111
+ type CustomerId = BrandedId<'Customer'>;
873
112
 
874
- ### Custom Validators
875
-
876
- ```typescript
877
- // Custom validator for credit card numbers
878
- class CreditCardValidator {
879
- public static isValid(cardNumber: string): boolean {
880
- // Remove spaces and hyphens
881
- const cleaned = cardNumber.replace(/[\s-]/g, '');
882
-
883
- // Check if all digits
884
- if (!/^\d+$/.test(cleaned)) {
885
- return false;
886
- }
887
-
888
- // Check length
889
- if (cleaned.length < 13 || cleaned.length > 19) {
890
- return false;
891
- }
892
-
893
- // Luhn algorithm
894
- return this.luhnCheck(cleaned);
895
- }
113
+ const orderId: OrderId = newBrandedId<'Order'>();
114
+ const customerId: CustomerId = newBrandedId<'Customer'>();
896
115
 
897
- private static luhnCheck(cardNumber: string): boolean {
898
- let sum = 0;
899
- let alternate = false;
900
-
901
- for (let i = cardNumber.length - 1; i >= 0; i--) {
902
- let digit = parseInt(cardNumber.charAt(i));
903
-
904
- if (alternate) {
905
- digit *= 2;
906
- if (digit > 9) {
907
- digit = (digit % 10) + 1;
908
- }
909
- }
910
-
911
- sum += digit;
912
- alternate = !alternate;
913
- }
914
-
915
- return sum % 10 === 0;
916
- }
116
+ function shipOrder(id: OrderId): void {
117
+ /* ... */
917
118
  }
918
119
 
919
- // Usage
920
- console.log(CreditCardValidator.isValid('4532 1234 5678 9012')); // true
921
- console.log(CreditCardValidator.isValid('1234 5678 9012 3456')); // false
922
- ```
923
-
924
- ## 📋 Serialization
925
-
926
- ### JSON Serialization
927
-
928
- ```typescript
929
- // All value objects support JSON serialization
930
- const money = new Money(100, 'USD');
931
- const email = new Email('user@example.com');
932
- const address = new Address('123 Main St', 'City', 'NY', '12345', 'USA');
933
-
934
- // Serialize to JSON
935
- const moneyJson = money.toJSON();
936
- console.log(moneyJson); // { amount: 100, currency: 'USD' }
937
-
938
- const emailJson = email.toJSON();
939
- console.log(emailJson); // { value: 'user@example.com' }
940
-
941
- const addressJson = address.toJSON();
942
- console.log(addressJson);
943
- // { street: '123 Main St', city: 'City', state: 'NY', zipCode: '12345', country: 'USA' }
944
- ```
945
-
946
- ### Deserialization
947
-
948
- ```typescript
949
- // Deserialize from JSON
950
- const deserializedMoney = Money.fromJSON(moneyJson);
951
- console.log(deserializedMoney.equals(money)); // true
952
-
953
- const deserializedEmail = Email.fromJSON(emailJson);
954
- console.log(deserializedEmail.equals(email)); // true
955
-
956
- const deserializedAddress = Address.fromJSON(addressJson);
957
- console.log(deserializedAddress.equals(address)); // true
958
- ```
959
-
960
- ### Custom Serialization
961
-
962
- ```typescript
963
- // Custom serialization for complex value objects
964
- class PersonName extends ValueObject {
965
- constructor(
966
- private readonly firstName: string,
967
- private readonly lastName: string,
968
- private readonly middleName?: string
969
- ) {
970
- super();
971
- }
972
-
973
- public get FirstName(): string {
974
- return this.firstName;
975
- }
976
-
977
- public get LastName(): string {
978
- return this.lastName;
979
- }
980
-
981
- public get MiddleName(): string | undefined {
982
- return this.middleName;
983
- }
984
-
985
- public getFullName(): string {
986
- const parts = [this.firstName];
987
- if (this.middleName) {
988
- parts.push(this.middleName);
989
- }
990
- parts.push(this.lastName);
991
- return parts.join(' ');
992
- }
993
-
994
- public toJSON(): object {
995
- return {
996
- firstName: this.firstName,
997
- lastName: this.lastName,
998
- middleName: this.middleName,
999
- fullName: this.getFullName(),
1000
- };
1001
- }
1002
-
1003
- public static fromJSON(json: any): PersonName {
1004
- return new PersonName(json.firstName, json.lastName, json.middleName);
1005
- }
1006
-
1007
- protected getEqualityComponents(): any[] {
1008
- return [this.firstName, this.lastName, this.middleName];
1009
- }
1010
- }
1011
- ```
1012
-
1013
- ## 🧪 Testing
1014
-
1015
- ### Unit Testing Value Objects
1016
-
1017
- ```typescript
1018
- import { describe, it, expect } from 'vitest';
1019
- import { Money, Email, Address } from '@vytches/ddd-value-objects';
1020
-
1021
- describe('Money', () => {
1022
- it('should create money with valid amount and currency', () => {
1023
- // Arrange & Act
1024
- const money = new Money(100, 'USD');
1025
-
1026
- // Assert
1027
- expect(money.amount).toBe(100);
1028
- expect(money.currency).toBe('USD');
1029
- });
1030
-
1031
- it('should add money with same currency', () => {
1032
- // Arrange
1033
- const money1 = new Money(100, 'USD');
1034
- const money2 = new Money(50, 'USD');
1035
-
1036
- // Act
1037
- const result = money1.add(money2);
1038
-
1039
- // Assert
1040
- expect(result.amount).toBe(150);
1041
- expect(result.currency).toBe('USD');
1042
- });
1043
-
1044
- it('should throw error when adding different currencies', () => {
1045
- // Arrange
1046
- const money1 = new Money(100, 'USD');
1047
- const money2 = new Money(50, 'EUR');
1048
-
1049
- // Act & Assert
1050
- expect(() => money1.add(money2)).toThrow(
1051
- 'Cannot operate on different currencies'
1052
- );
1053
- });
1054
- });
1055
-
1056
- describe('Email', () => {
1057
- it('should create email with valid address', () => {
1058
- // Arrange & Act
1059
- const email = new Email('user@example.com');
1060
-
1061
- // Assert
1062
- expect(email.value).toBe('user@example.com');
1063
- expect(email.localPart).toBe('user');
1064
- expect(email.domain).toBe('example.com');
1065
- });
1066
-
1067
- it('should normalize email to lowercase', () => {
1068
- // Arrange & Act
1069
- const email = new Email('USER@EXAMPLE.COM');
1070
-
1071
- // Assert
1072
- expect(email.value).toBe('user@example.com');
1073
- });
1074
-
1075
- it('should throw error for invalid email', () => {
1076
- // Act & Assert
1077
- expect(() => new Email('invalid-email')).toThrow('Invalid email format');
1078
- });
1079
- });
1080
- ```
1081
-
1082
- ### Test Builders
1083
-
1084
- ```typescript
1085
- // Test builder for complex value objects
1086
- class AddressBuilder {
1087
- private street: string = '123 Main St';
1088
- private city: string = 'Anytown';
1089
- private state: string = 'NY';
1090
- private zipCode: string = '12345';
1091
- private country: string = 'USA';
1092
-
1093
- public withStreet(street: string): AddressBuilder {
1094
- this.street = street;
1095
- return this;
1096
- }
1097
-
1098
- public withCity(city: string): AddressBuilder {
1099
- this.city = city;
1100
- return this;
1101
- }
1102
-
1103
- public withState(state: string): AddressBuilder {
1104
- this.state = state;
1105
- return this;
1106
- }
1107
-
1108
- public withZipCode(zipCode: string): AddressBuilder {
1109
- this.zipCode = zipCode;
1110
- return this;
1111
- }
1112
-
1113
- public withCountry(country: string): AddressBuilder {
1114
- this.country = country;
1115
- return this;
1116
- }
1117
-
1118
- public build(): Address {
1119
- return new Address(
1120
- this.street,
1121
- this.city,
1122
- this.state,
1123
- this.zipCode,
1124
- this.country
1125
- );
1126
- }
1127
- }
1128
-
1129
- // Usage in tests
1130
- describe('Address', () => {
1131
- it('should create address with all components', () => {
1132
- // Arrange
1133
- const address = new AddressBuilder()
1134
- .withStreet('456 Oak Ave')
1135
- .withCity('Springfield')
1136
- .withState('IL')
1137
- .withZipCode('62701')
1138
- .build();
1139
-
1140
- // Assert
1141
- expect(address.street).toBe('456 Oak Ave');
1142
- expect(address.city).toBe('Springfield');
1143
- expect(address.state).toBe('IL');
1144
- expect(address.zipCode).toBe('62701');
1145
- });
1146
- });
1147
- ```
1148
-
1149
- ### Property-Based Testing
1150
-
1151
- ```typescript
1152
- import { fc } from 'fast-check';
1153
-
1154
- // Property-based testing for Money
1155
- describe('Money Properties', () => {
1156
- it('should be associative for addition', () => {
1157
- fc.assert(
1158
- fc.property(
1159
- fc.float({ min: 0, max: 1000000 }),
1160
- fc.float({ min: 0, max: 1000000 }),
1161
- fc.float({ min: 0, max: 1000000 }),
1162
- fc.constantFrom('USD', 'EUR', 'GBP'),
1163
- (a, b, c, currency) => {
1164
- const money1 = new Money(a, currency);
1165
- const money2 = new Money(b, currency);
1166
- const money3 = new Money(c, currency);
1167
-
1168
- // (a + b) + c = a + (b + c)
1169
- const left = money1.add(money2).add(money3);
1170
- const right = money1.add(money2.add(money3));
1171
-
1172
- expect(left.equals(right)).toBe(true);
1173
- }
1174
- )
1175
- );
1176
- });
1177
- });
1178
- ```
1179
-
1180
- ## 🏆 Best Practices
1181
-
1182
- ### Value Object Design
1183
-
1184
- ```typescript
1185
- // ✅ Good: Immutable, validated, rich behavior
1186
- class ISBN extends ValueObject {
1187
- private readonly value: string;
1188
-
1189
- constructor(isbn: string) {
1190
- super();
1191
- this.value = this.normalize(isbn);
1192
- }
1193
-
1194
- public get Value(): string {
1195
- return this.value;
1196
- }
1197
-
1198
- public isISBN10(): boolean {
1199
- return this.value.length === 10;
1200
- }
1201
-
1202
- public isISBN13(): boolean {
1203
- return this.value.length === 13;
1204
- }
1205
-
1206
- public format(): string {
1207
- if (this.isISBN13()) {
1208
- return `${this.value.slice(0, 3)}-${this.value.slice(3, 4)}-${this.value.slice(4, 6)}-${this.value.slice(6, 12)}-${this.value.slice(12)}`;
1209
- } else {
1210
- return `${this.value.slice(0, 1)}-${this.value.slice(1, 4)}-${this.value.slice(4, 9)}-${this.value.slice(9)}`;
1211
- }
1212
- }
1213
-
1214
- protected getEqualityComponents(): any[] {
1215
- return [this.value];
1216
- }
1217
-
1218
- private normalize(isbn: string): string {
1219
- const cleaned = isbn.replace(/[\s-]/g, '');
1220
-
1221
- if (!/^[\d]{9}[\dX]$/.test(cleaned) && !/^[\d]{13}$/.test(cleaned)) {
1222
- throw new ValidationError('Invalid ISBN format', 'isbn', isbn);
1223
- }
1224
-
1225
- return cleaned;
1226
- }
1227
- }
1228
-
1229
- // ❌ Bad: Mutable, no validation
1230
- class ISBN {
1231
- public value: string;
1232
-
1233
- constructor(isbn: string) {
1234
- this.value = isbn; // No validation
1235
- }
1236
- }
1237
- ```
1238
-
1239
- ### Validation Strategy
1240
-
1241
- ```typescript
1242
- // ✅ Good: Comprehensive validation
1243
- class CreditCard extends ValueObject {
1244
- private readonly number: string;
1245
- private readonly type: CreditCardType;
1246
-
1247
- constructor(cardNumber: string) {
1248
- super();
1249
- this.number = this.normalize(cardNumber);
1250
- this.type = this.detectType(this.number);
1251
- }
1252
-
1253
- public get Number(): string {
1254
- return this.number;
1255
- }
1256
-
1257
- public get Type(): CreditCardType {
1258
- return this.type;
1259
- }
1260
-
1261
- public getMaskedNumber(): string {
1262
- const last4 = this.number.slice(-4);
1263
- const masked = '*'.repeat(this.number.length - 4);
1264
- return `${masked}${last4}`;
1265
- }
1266
-
1267
- protected getEqualityComponents(): any[] {
1268
- return [this.number];
1269
- }
1270
-
1271
- private normalize(cardNumber: string): string {
1272
- const cleaned = cardNumber.replace(/[\s-]/g, '');
1273
-
1274
- if (!/^\d+$/.test(cleaned)) {
1275
- throw new ValidationError(
1276
- 'Credit card number must contain only digits',
1277
- 'cardNumber',
1278
- cardNumber
1279
- );
1280
- }
1281
-
1282
- if (cleaned.length < 13 || cleaned.length > 19) {
1283
- throw new ValidationError(
1284
- 'Credit card number must be between 13 and 19 digits',
1285
- 'cardNumber',
1286
- cardNumber
1287
- );
1288
- }
1289
-
1290
- if (!this.luhnCheck(cleaned)) {
1291
- throw new ValidationError(
1292
- 'Invalid credit card number',
1293
- 'cardNumber',
1294
- cardNumber
1295
- );
1296
- }
1297
-
1298
- return cleaned;
1299
- }
1300
-
1301
- private detectType(number: string): CreditCardType {
1302
- if (number.startsWith('4')) return CreditCardType.VISA;
1303
- if (number.startsWith('5') || number.startsWith('2'))
1304
- return CreditCardType.MASTERCARD;
1305
- if (number.startsWith('3')) return CreditCardType.AMEX;
1306
- return CreditCardType.UNKNOWN;
1307
- }
1308
-
1309
- private luhnCheck(number: string): boolean {
1310
- // Implementation details...
1311
- return true;
1312
- }
1313
- }
1314
- ```
1315
-
1316
- ### Error Handling
1317
-
1318
- ```typescript
1319
- // ✅ Good: Specific error types and messages
1320
- class TaxId extends ValueObject {
1321
- private readonly value: string;
1322
-
1323
- constructor(taxId: string) {
1324
- super();
1325
- this.value = this.validate(taxId);
1326
- }
1327
-
1328
- private validate(taxId: string): string {
1329
- if (!taxId) {
1330
- throw new ValidationError('Tax ID cannot be empty', 'taxId', taxId);
1331
- }
1332
-
1333
- const cleaned = taxId.replace(/[\s-]/g, '');
1334
-
1335
- if (!/^\d{9}$/.test(cleaned)) {
1336
- throw new ValidationError('Tax ID must be 9 digits', 'taxId', taxId);
1337
- }
1338
-
1339
- if (cleaned === '000000000') {
1340
- throw new ValidationError('Tax ID cannot be all zeros', 'taxId', taxId);
1341
- }
1342
-
1343
- return cleaned;
1344
- }
1345
- }
1346
-
1347
- // ❌ Bad: Generic errors
1348
- class TaxId {
1349
- constructor(taxId: string) {
1350
- if (!taxId) {
1351
- throw new Error('Invalid'); // Too generic
1352
- }
1353
- }
1354
- }
1355
- ```
1356
-
1357
- ## 🤝 Contributing
1358
-
1359
- We welcome contributions! Please see our
1360
- [Contributing Guide](../../CONTRIBUTING.md) for details.
1361
-
1362
- ### Development Setup
1363
-
1364
- ```bash
1365
- # Clone repository
1366
- git clone https://github.com/vytches/ddd.git
1367
- cd ddd
1368
-
1369
- # Install dependencies
1370
- pnpm install
1371
-
1372
- # Build package
1373
- pnpm build --filter=@vytches/ddd-value-objects
1374
-
1375
- # Run tests
1376
- pnpm test --filter=@vytches/ddd-value-objects
1377
-
1378
- # Run in development mode
1379
- pnpm dev --filter=@vytches/ddd-value-objects
120
+ shipOrder(orderId); // OK
121
+ shipOrder(customerId); // TypeScript compile error!
1380
122
  ```
1381
123
 
1382
- ## 📄 License
124
+ ## Package boundaries
1383
125
 
1384
- This project is licensed under the MIT License - see the
1385
- [LICENSE](../../LICENSE) file for details.
126
+ `@vytches/ddd-value-objects` depends on:
1386
127
 
1387
- ---
128
+ - `@vytches/ddd-contracts` — `EntityId` base class, `IEntityId`, `IdType`
129
+ - `@vytches/ddd-domain-primitives` — error types
130
+ - `@vytches/ddd-utils` — `LibUtils`, `Result`
1388
131
 
1389
- **Part of the [@vytches/ddd-core](https://github.com/vytches/ddd) ecosystem**
132
+ ## License
1390
133
 
1391
- For more information, visit the [main documentation](../../README.md).
134
+ MIT