@haskou/value-objects 1.0.1

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 (85) hide show
  1. package/.editorconfig +15 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.json +218 -0
  4. package/.prettierrc.json +13 -0
  5. package/README.md +156 -0
  6. package/TECHNICAL_DOCUMENTATION.md +1091 -0
  7. package/jest.config.ts +30 -0
  8. package/package.json +49 -0
  9. package/src/errors/BaseError.ts +20 -0
  10. package/src/errors/DomainError.ts +12 -0
  11. package/src/errors/InvalidColorError.ts +7 -0
  12. package/src/errors/InvalidDayError.ts +7 -0
  13. package/src/errors/InvalidDayFormatError.ts +7 -0
  14. package/src/errors/InvalidEmailError.ts +7 -0
  15. package/src/errors/InvalidHourError.ts +7 -0
  16. package/src/errors/InvalidIntegerError.ts +7 -0
  17. package/src/errors/InvalidLatitudeError.ts +7 -0
  18. package/src/errors/InvalidLongitudeError.ts +7 -0
  19. package/src/errors/InvalidMinutesError.ts +7 -0
  20. package/src/errors/InvalidNumberError.ts +7 -0
  21. package/src/errors/InvalidPositiveNumberError.ts +7 -0
  22. package/src/errors/InvalidStringLengthError.ts +9 -0
  23. package/src/errors/InvalidTimestampIntervalError.ts +10 -0
  24. package/src/errors/NullObjectError.ts +8 -0
  25. package/src/errors/ValueNotInEnumError.ts +9 -0
  26. package/src/errors/index.ts +17 -0
  27. package/src/index.ts +5 -0
  28. package/src/interfaces/PrimitiveOf.ts +5 -0
  29. package/src/interfaces/index.ts +1 -0
  30. package/src/patterns/Assert.ts +15 -0
  31. package/src/patterns/NullObject.ts +60 -0
  32. package/src/patterns/ValueObject.ts +40 -0
  33. package/src/patterns/index.ts +3 -0
  34. package/src/types/Nullish.ts +1 -0
  35. package/src/types/Primitive.ts +1 -0
  36. package/src/types/index.ts +2 -0
  37. package/src/value-objects/Color.ts +39 -0
  38. package/src/value-objects/Email.ts +23 -0
  39. package/src/value-objects/Enum.ts +31 -0
  40. package/src/value-objects/Integer.ts +22 -0
  41. package/src/value-objects/NumberValueObject.ts +56 -0
  42. package/src/value-objects/PositiveNumber.ts +20 -0
  43. package/src/value-objects/StringValueObject.ts +27 -0
  44. package/src/value-objects/coordinates/Coordinates.ts +30 -0
  45. package/src/value-objects/coordinates/Latitude.ts +22 -0
  46. package/src/value-objects/coordinates/Longitude.ts +25 -0
  47. package/src/value-objects/coordinates/index.ts +3 -0
  48. package/src/value-objects/index.ts +9 -0
  49. package/src/value-objects/time/CalendarDay.ts +91 -0
  50. package/src/value-objects/time/Day.ts +17 -0
  51. package/src/value-objects/time/DayOfWeek.ts +60 -0
  52. package/src/value-objects/time/Duration.ts +142 -0
  53. package/src/value-objects/time/Hour.ts +105 -0
  54. package/src/value-objects/time/Month.ts +39 -0
  55. package/src/value-objects/time/MonthOfYear.ts +52 -0
  56. package/src/value-objects/time/Timestamp.ts +208 -0
  57. package/src/value-objects/time/TimestampInterval.ts +122 -0
  58. package/src/value-objects/time/Year.ts +27 -0
  59. package/src/value-objects/time/index.ts +10 -0
  60. package/tests/errors/BaseError.spec.ts +63 -0
  61. package/tests/errors/DomainError.spec.ts +52 -0
  62. package/tests/patterns/Assert.spec.ts +29 -0
  63. package/tests/patterns/NullObject.spec.ts +55 -0
  64. package/tests/setup.jest.ts +2 -0
  65. package/tests/value-objects/Color.spec.ts +214 -0
  66. package/tests/value-objects/Email.spec.ts +145 -0
  67. package/tests/value-objects/Enum.spec.ts +293 -0
  68. package/tests/value-objects/Integer.spec.ts +38 -0
  69. package/tests/value-objects/NumberValueObject.spec.ts +446 -0
  70. package/tests/value-objects/PositiveNumber.spec.ts +274 -0
  71. package/tests/value-objects/StringValueObject.spec.ts +135 -0
  72. package/tests/value-objects/coordinates/Coordinates.spec.ts +90 -0
  73. package/tests/value-objects/coordinates/Latitude.spec.ts +24 -0
  74. package/tests/value-objects/coordinates/Longitude.spec.ts +24 -0
  75. package/tests/value-objects/time/CalendarDay.spec.ts +182 -0
  76. package/tests/value-objects/time/Day.spec.ts +29 -0
  77. package/tests/value-objects/time/DayOfWeek.spec.ts +71 -0
  78. package/tests/value-objects/time/Duration.spec.ts +278 -0
  79. package/tests/value-objects/time/Hour.spec.ts +197 -0
  80. package/tests/value-objects/time/MonthOfYear.spec.ts +111 -0
  81. package/tests/value-objects/time/Timestamp.spec.ts +497 -0
  82. package/tests/value-objects/time/TimestampInterval.spec.ts +383 -0
  83. package/tests/value-objects/time/Year.spec.ts +48 -0
  84. package/tsconfig.jest.json +33 -0
  85. package/tsconfig.json +42 -0
@@ -0,0 +1,1091 @@
1
+ # Technical Documentation
2
+
3
+ Comprehensive technical documentation for the Value Objects library.
4
+
5
+ ## 📋 Table of Contents
6
+
7
+ - [API Documentation](#api-documentation)
8
+ - [Base Classes](#base-classes)
9
+ - [String Value Objects](#string-value-objects)
10
+ - [Number Value Objects](#number-value-objects)
11
+ - [Integer Value Objects](#integer-value-objects)
12
+ - [PositiveNumber Value Objects](#positivenumber-value-objects)
13
+ - [Year Value Objects](#year-value-objects)
14
+ - [Color Value Objects](#color-value-objects)
15
+ - [Email Value Objects](#email-value-objects)
16
+ - [Hour Value Objects](#hour-value-objects)
17
+ - [Time Value Objects](#time-value-objects)
18
+ - [Enum Value Objects](#enum-value-objects)
19
+ - [Error Handling](#error-handling)
20
+ - [Design Principles](#design-principles)
21
+
22
+ ## 📚 API Documentation
23
+
24
+ ### Base Classes
25
+
26
+ #### ValueObject\<T>
27
+
28
+ The abstract base class for all value objects.
29
+
30
+ ```typescript
31
+ abstract class ValueObject<T extends Primitive = Primitive> {
32
+ constructor(protected readonly value: T);
33
+
34
+ public isEqual(other: unknown): boolean;
35
+ public valueOf(): T;
36
+ public toString(): string;
37
+ protected clone(value: T): this;
38
+ }
39
+ ```
40
+
41
+ **Methods:**
42
+ - `valueOf()`: Returns the primitive value
43
+ - `toString()`: Returns string representation
44
+ - `isEqual(other)`: Compares equality with another value
45
+ - `clone(value)`: Creates a new instance with the given value
46
+
47
+ ### String Value Objects
48
+
49
+ #### StringValueObject
50
+
51
+ Represents immutable string values with length validation.
52
+
53
+ ```typescript
54
+ class StringValueObject extends ValueObject<string> {
55
+ constructor(value: string | StringValueObject, maxLength?: number);
56
+
57
+ public toString(): string;
58
+ public isEmpty(): boolean;
59
+ }
60
+ ```
61
+
62
+ **Example:**
63
+ ```typescript
64
+ // Basic usage
65
+ const username = new StringValueObject('alice123');
66
+
67
+ // With length limit
68
+ const shortCode = new StringValueObject('ABC', 3);
69
+
70
+ // From another StringValueObject
71
+ const copy = new StringValueObject(username);
72
+
73
+ // Validation
74
+ try {
75
+ new StringValueObject('a'.repeat(600)); // Throws InvalidStringLengthError
76
+ } catch (error) {
77
+ console.error('String too long');
78
+ }
79
+ ```
80
+
81
+ ### Number Value Objects
82
+
83
+ #### NumberValueObject
84
+
85
+ Represents immutable numeric values with arithmetic operations.
86
+
87
+ ```typescript
88
+ class NumberValueObject extends ValueObject<number> {
89
+ constructor(value: number | NumberValueObject);
90
+
91
+ // Comparison methods
92
+ public isZero(): boolean;
93
+ public isGreaterThan(other: number | NumberValueObject): boolean;
94
+ public isGreaterOrEqualThan(other: number | NumberValueObject): boolean;
95
+ public isLessThan(other: number | NumberValueObject): boolean;
96
+ public isLessOrEqualThan(other: number | NumberValueObject): boolean;
97
+
98
+ // Arithmetic operations
99
+ public add(other: number | NumberValueObject): NumberValueObject;
100
+ public subtract(other: number | NumberValueObject): NumberValueObject;
101
+ public multiply(other: number | NumberValueObject): NumberValueObject;
102
+ public divide(other: number | NumberValueObject): NumberValueObject;
103
+ }
104
+ ```
105
+
106
+ **Example:**
107
+ ```typescript
108
+ const a = new NumberValueObject(10);
109
+ const b = new NumberValueObject(5);
110
+
111
+ // Arithmetic operations (immutable)
112
+ const sum = a.add(b); // 15
113
+ const diff = a.subtract(b); // 5
114
+ const prod = a.multiply(b); // 50
115
+ const quot = a.divide(b); // 2
116
+
117
+ // Comparisons
118
+ console.log(a.isGreaterThan(b)); // true
119
+ console.log(b.isLessThan(a)); // true
120
+ console.log(a.isEqual(10)); // true
121
+
122
+ // Works with primitive numbers
123
+ const result = a.add(3); // 13
124
+
125
+ // Original values remain unchanged
126
+ console.log(a.valueOf()); // 10 (unchanged)
127
+ console.log(b.valueOf()); // 5 (unchanged)
128
+ ```
129
+
130
+ ### Integer Value Objects
131
+
132
+ #### Integer
133
+
134
+ Represents immutable integer values (whole numbers) with all arithmetic operations inherited from NumberValueObject.
135
+
136
+ ```typescript
137
+ class Integer extends NumberValueObject {
138
+ constructor(value: number | NumberValueObject);
139
+
140
+ // Inherits all NumberValueObject methods:
141
+ // Comparison methods
142
+ public isZero(): boolean;
143
+ public isGreaterThan(other: number | NumberValueObject): boolean;
144
+ public isGreaterOrEqualThan(other: number | NumberValueObject): boolean;
145
+ public isLessThan(other: number | NumberValueObject): boolean;
146
+ public isLessOrEqualThan(other: number | NumberValueObject): boolean;
147
+
148
+ // Arithmetic operations
149
+ public add(other: number | NumberValueObject): NumberValueObject;
150
+ public subtract(other: number | NumberValueObject): NumberValueObject;
151
+ public multiply(other: number | NumberValueObject): NumberValueObject;
152
+ public divide(other: number | NumberValueObject): NumberValueObject;
153
+ }
154
+ ```
155
+
156
+ **Example:**
157
+ ```typescript
158
+ // Valid integers
159
+ const count = new Integer(42);
160
+ const negative = new Integer(-10);
161
+ const zero = new Integer(0);
162
+
163
+ // Arithmetic operations (inherited from NumberValueObject)
164
+ const sum = count.add(negative); // 32
165
+ const product = count.multiply(2); // 84
166
+ const quotient = count.divide(2); // 21
167
+
168
+ // Comparisons
169
+ console.log(count.isGreaterThan(zero)); // true
170
+ console.log(negative.isLessThan(zero)); // true
171
+ console.log(zero.isZero()); // true
172
+
173
+ // From another NumberValueObject
174
+ const numberValue = new NumberValueObject(15);
175
+ const integerFromNumber = new Integer(numberValue);
176
+ console.log(integerFromNumber.valueOf()); // 15
177
+
178
+ // String representation
179
+ console.log(count.toString()); // '42'
180
+ console.log(negative.valueOf()); // -10
181
+
182
+ // Note: Arithmetic operations return NumberValueObject, not Integer
183
+ // This is because operations might result in non-integer values
184
+ const division = count.divide(3); // Returns NumberValueObject with value 14
185
+
186
+ // Validation
187
+ try {
188
+ new Integer(42.5); // Throws InvalidIntegerError
189
+ new Integer(3.14159); // Throws InvalidIntegerError
190
+ new Integer(Infinity); // Throws InvalidIntegerError
191
+ new Integer(-Infinity); // Throws InvalidIntegerError
192
+ new Integer(NaN); // Throws InvalidNumberError
193
+ } catch (error) {
194
+ console.error('Value must be a valid integer');
195
+ }
196
+
197
+ // Works with large integers
198
+ const large = new Integer(1000000); // Valid
199
+ const veryLarge = new Integer(-999999); // Valid
200
+ ```
201
+
202
+ ### PositiveNumber Value Objects
203
+
204
+ #### PositiveNumber
205
+
206
+ Represents immutable positive numeric values (greater than 0) with all arithmetic operations inherited from NumberValueObject.
207
+
208
+ ```typescript
209
+ class PositiveNumber extends NumberValueObject {
210
+ constructor(value: number | NumberValueObject);
211
+
212
+ // Inherits all NumberValueObject methods:
213
+ // Comparison methods
214
+ public isZero(): boolean; // Always returns false
215
+ public isGreaterThan(other: number | NumberValueObject): boolean;
216
+ public isGreaterOrEqualThan(other: number | NumberValueObject): boolean;
217
+ public isLessThan(other: number | NumberValueObject): boolean;
218
+ public isLessOrEqualThan(other: number | NumberValueObject): boolean;
219
+
220
+ // Arithmetic operations
221
+ public add(other: number | NumberValueObject): NumberValueObject;
222
+ public subtract(other: number | NumberValueObject): NumberValueObject;
223
+ public multiply(other: number | NumberValueObject): NumberValueObject;
224
+ public divide(other: number | NumberValueObject): NumberValueObject;
225
+ }
226
+ ```
227
+
228
+ **Example:**
229
+ ```typescript
230
+ // Valid positive numbers
231
+ const quantity = new PositiveNumber(5);
232
+ const price = new PositiveNumber(19.99);
233
+ const percentage = new PositiveNumber(0.15);
234
+
235
+ // Arithmetic operations (inherited from NumberValueObject)
236
+ const total = quantity.multiply(price); // 99.95
237
+ const increased = price.add(10); // 29.99
238
+ const half = quantity.divide(2); // 2.5
239
+
240
+ // Comparisons
241
+ console.log(price.isGreaterThan(quantity)); // true
242
+ console.log(quantity.isLessThan(price)); // true
243
+ console.log(quantity.isZero()); // false (always false for PositiveNumber)
244
+
245
+ // From another PositiveNumber
246
+ const copy = new PositiveNumber(quantity);
247
+ console.log(copy.valueOf()); // 5
248
+
249
+ // String representation
250
+ console.log(quantity.toString()); // '5'
251
+ console.log(price.valueOf()); // 19.99
252
+
253
+ // Note: Arithmetic operations return NumberValueObject, not PositiveNumber
254
+ // This is because operations might result in non-positive values
255
+ const result = quantity.subtract(10); // Returns NumberValueObject with value -5
256
+
257
+ // Validation
258
+ try {
259
+ new PositiveNumber(0); // Throws InvalidPositiveNumberError
260
+ new PositiveNumber(-5); // Throws InvalidPositiveNumberError
261
+ new PositiveNumber(-0.1); // Throws InvalidPositiveNumberError
262
+ } catch (error) {
263
+ console.error('Value must be greater than 0');
264
+ }
265
+
266
+ // Works with decimals
267
+ const decimal = new PositiveNumber(0.001); // Valid
268
+ const large = new PositiveNumber(1000000); // Valid
269
+ ```
270
+
271
+ ### Year Value Objects
272
+
273
+ #### Year
274
+
275
+ Represents immutable year values with leap year calculations and date utilities. Inherits all functionality from Integer.
276
+
277
+ ```typescript
278
+ class Year extends Integer {
279
+ constructor(value: number | NumberValueObject);
280
+
281
+ // Year-specific methods
282
+ public isLeapYear(): boolean;
283
+ public getNumberOfDays(): number;
284
+
285
+ // Inherits all Integer and NumberValueObject methods:
286
+ // Comparison methods
287
+ public isZero(): boolean;
288
+ public isGreaterThan(other: number | NumberValueObject): boolean;
289
+ public isGreaterOrEqualThan(other: number | NumberValueObject): boolean;
290
+ public isLessThan(other: number | NumberValueObject): boolean;
291
+ public isLessOrEqualThan(other: number | NumberValueObject): boolean;
292
+
293
+ // Arithmetic operations
294
+ public add(other: number | NumberValueObject): NumberValueObject;
295
+ public subtract(other: number | NumberValueObject): NumberValueObject;
296
+ public multiply(other: number | NumberValueObject): NumberValueObject;
297
+ public divide(other: number | NumberValueObject): NumberValueObject;
298
+ }
299
+ ```
300
+
301
+ **Example:**
302
+ ```typescript
303
+ // Valid years
304
+ const currentYear = new Year(2024);
305
+ const pastYear = new Year(1900);
306
+ const futureYear = new Year(2100);
307
+ const year2000 = new Year(2000);
308
+
309
+ // Leap year calculations
310
+ console.log(currentYear.isLeapYear()); // true (2024 is divisible by 4)
311
+ console.log(pastYear.isLeapYear()); // false (1900 is divisible by 100 but not 400)
312
+ console.log(futureYear.isLeapYear()); // false (2100 is divisible by 100 but not 400)
313
+ console.log(year2000.isLeapYear()); // true (2000 is divisible by 400)
314
+
315
+ // Number of days in year
316
+ console.log(currentYear.getNumberOfDays()); // 366 (leap year)
317
+ console.log(pastYear.getNumberOfDays()); // 365 (regular year)
318
+ console.log(year2000.getNumberOfDays()); // 366 (leap year)
319
+
320
+ // Arithmetic operations (inherited from NumberValueObject)
321
+ const nextYear = currentYear.add(1); // 2025 (as NumberValueObject)
322
+ const decade = currentYear.subtract(10); // 2014 (as NumberValueObject)
323
+
324
+ // Comparisons
325
+ console.log(currentYear.isGreaterThan(pastYear)); // true
326
+ console.log(pastYear.isLessThan(futureYear)); // true
327
+ console.log(currentYear.isEqual(2024)); // true
328
+
329
+ // From another NumberValueObject or Integer
330
+ const yearFromNumber = new NumberValueObject(2023);
331
+ const year = new Year(yearFromNumber);
332
+ console.log(year.valueOf()); // 2023
333
+ console.log(year.isLeapYear()); // false
334
+
335
+ // String representation
336
+ console.log(currentYear.toString()); // '2024'
337
+ console.log(pastYear.valueOf()); // 1900
338
+
339
+ // Common leap year patterns
340
+ const leapYears = [2020, 2024, 2028, 2032].map(y => new Year(y));
341
+ const regularYears = [2021, 2022, 2023, 2025].map(y => new Year(y));
342
+
343
+ leapYears.forEach(year => {
344
+ console.log(`${year.valueOf()}: ${year.getNumberOfDays()} days`); // All show 366
345
+ });
346
+
347
+ regularYears.forEach(year => {
348
+ console.log(`${year.valueOf()}: ${year.getNumberOfDays()} days`); // All show 365
349
+ });
350
+
351
+ // Validation (inherits Integer validation)
352
+ try {
353
+ new Year(2024.5); // Throws InvalidIntegerError
354
+ new Year(NaN); // Throws InvalidNumberError
355
+ new Year(Infinity); // Throws InvalidIntegerError
356
+ } catch (error) {
357
+ console.error('Year must be a valid integer');
358
+ }
359
+
360
+ // Works with negative years (BCE)
361
+ const ancientYear = new Year(-500); // 500 BCE
362
+ console.log(ancientYear.valueOf()); // -500
363
+ console.log(ancientYear.isLeapYear()); // false (year -500 is not a leap year)
364
+ ```
365
+
366
+ ### Color Value Objects
367
+
368
+ #### Color
369
+
370
+ Represents immutable hex color values with predefined colors.
371
+
372
+ ```typescript
373
+ class Color extends StringValueObject {
374
+ constructor(value: string | StringValueObject);
375
+
376
+ public isEqual(other: Color): boolean; // Case-insensitive comparison
377
+
378
+ // Predefined static colors
379
+ static readonly RED: Color;
380
+ static readonly GREEN: Color;
381
+ static readonly BLUE: Color;
382
+ static readonly BLACK: Color;
383
+ static readonly WHITE: Color;
384
+ static readonly YELLOW: Color;
385
+ static readonly CYAN: Color;
386
+ static readonly MAGENTA: Color;
387
+ static readonly ORANGE: Color;
388
+ static readonly PURPLE: Color;
389
+ static readonly PINK: Color;
390
+ static readonly BROWN: Color;
391
+ }
392
+ ```
393
+
394
+ **Example:**
395
+ ```typescript
396
+ // Valid hex colors
397
+ const red = new Color('#FF0000');
398
+ const blue = new Color('#00F');
399
+ const green = Color.GREEN;
400
+
401
+ // Case-insensitive comparison
402
+ const color1 = new Color('#ff0000');
403
+ const color2 = new Color('#FF0000');
404
+ console.log(color1.isEqual(color2)); // true
405
+
406
+ // From StringValueObject
407
+ const colorString = new StringValueObject('#ABCDEF');
408
+ const color = new Color(colorString);
409
+
410
+ // Validation
411
+ try {
412
+ new Color('invalid-color'); // Throws InvalidColorError
413
+ } catch (error) {
414
+ console.error('Invalid hex color format');
415
+ }
416
+ ```
417
+
418
+ ### Email Value Objects
419
+
420
+ #### Email
421
+
422
+ Represents immutable email addresses with comprehensive validation.
423
+
424
+ ```typescript
425
+ class Email extends StringValueObject {
426
+ constructor(value: string | StringValueObject);
427
+ }
428
+ ```
429
+
430
+ **Example:**
431
+ ```typescript
432
+ // Valid email addresses
433
+ const userEmail = new Email('user@example.com');
434
+ const workEmail = new Email('john.doe@company.org');
435
+ const taggedEmail = new Email('user+newsletter@domain.co.uk');
436
+
437
+ // From StringValueObject
438
+ const emailString = new StringValueObject('admin@system.net');
439
+ const email = new Email(emailString);
440
+
441
+ // String representation
442
+ console.log(userEmail.toString()); // 'user@example.com'
443
+ console.log(userEmail.valueOf()); // 'user@example.com'
444
+
445
+ // Equality comparison
446
+ const email1 = new Email('test@example.com');
447
+ const email2 = new Email('test@example.com');
448
+ console.log(email1.isEqual(email2)); // true
449
+ console.log(email1.isEqual('test@example.com')); // true
450
+
451
+ // Inherits all StringValueObject methods
452
+ console.log(userEmail.isEmpty()); // false
453
+
454
+ // Validation examples
455
+ try {
456
+ new Email('invalid-email'); // Throws InvalidEmailError
457
+ new Email('user@'); // Throws InvalidEmailError
458
+ new Email('user@domain'); // Throws InvalidEmailError
459
+ new Email('user@@domain.com'); // Throws InvalidEmailError
460
+ } catch (error) {
461
+ console.error('Invalid email format');
462
+ }
463
+
464
+ // Supports various valid formats
465
+ const validEmails = [
466
+ 'simple@example.com',
467
+ 'user.name@domain.org',
468
+ 'user+tag@example.co.uk',
469
+ 'first.last@subdomain.company.travel',
470
+ 'admin123@my-domain.museum'
471
+ ];
472
+ ```
473
+
474
+ ### Hour Value Objects
475
+
476
+ #### Hour
477
+
478
+ Represents immutable time values in 24-hour format with time arithmetic operations.
479
+
480
+ ```typescript
481
+ class Hour extends StringValueObject {
482
+ constructor(value: string); // "HH:MM" format
483
+ constructor(value: number, minutes?: number); // Separate hours and minutes
484
+
485
+ // Time arithmetic
486
+ public addMinutes(minutes: number): Hour;
487
+ public diffInMinutes(other: Hour): number;
488
+
489
+ // Getters
490
+ public getHours(): number;
491
+ public getMinutes(): number;
492
+
493
+ // Comparisons
494
+ public isGreaterThan(hour: Hour): boolean;
495
+ public isLessThan(hour: Hour): boolean;
496
+ }
497
+ ```
498
+
499
+ **Example:**
500
+ ```typescript
501
+ // Different constructor options
502
+ const morning = new Hour('09:30'); // String format
503
+ const afternoon = new Hour(14, 45); // Separate values
504
+ const evening = new Hour(20, 0); // 20:00
505
+
506
+ // Time arithmetic
507
+ const laterTime = morning.addMinutes(90); // 11:00
508
+ const duration = morning.diffInMinutes(afternoon); // 315 minutes
509
+
510
+ // Getters
511
+ console.log(morning.getHours()); // 9
512
+ console.log(morning.getMinutes()); // 30
513
+
514
+ // Comparisons
515
+ console.log(afternoon.isGreaterThan(morning)); // true
516
+ console.log(morning.isLessThan(evening)); // true
517
+
518
+ // String representation
519
+ console.log(morning.toString()); // '09:30'
520
+
521
+ // Handles day overflow
522
+ const lateNight = new Hour('23:30');
523
+ const nextDay = lateNight.addMinutes(60); // '00:30' (next day)
524
+
525
+ // Validation
526
+ try {
527
+ new Hour('25:00'); // Throws InvalidHourError
528
+ new Hour(12, 65); // Throws InvalidMinutesError
529
+ } catch (error) {
530
+ console.error('Invalid time format');
531
+ }
532
+ ```
533
+
534
+ ### Time Value Objects
535
+
536
+ #### CalendarDay
537
+
538
+ Represents immutable calendar day values with date utilities.
539
+
540
+ ```typescript
541
+ class CalendarDay {
542
+ constructor(value?: string | Date | number | Timestamp);
543
+
544
+ public static fromTimestamp(timestamp: Timestamp): CalendarDay;
545
+ public static fromString(value: string): CalendarDay;
546
+
547
+ public toString(): string;
548
+ public getDayOfWeek(): DayOfWeek;
549
+ public isBefore(other: CalendarDay): boolean;
550
+ public isAfter(other: CalendarDay): boolean;
551
+ public diffInDays(other: CalendarDay): number;
552
+ }
553
+ ```
554
+
555
+ #### Day
556
+
557
+ Represents immutable day values (1-31) with validation.
558
+
559
+ ```typescript
560
+ class Day extends Integer {
561
+ constructor(value: number | NumberValueObject);
562
+ }
563
+ ```
564
+
565
+ #### DayOfWeek
566
+
567
+ Represents immutable day of week enumeration.
568
+
569
+ ```typescript
570
+ enum EDaysOfWeek {
571
+ MONDAY = 'monday',
572
+ TUESDAY = 'tuesday',
573
+ WEDNESDAY = 'wednesday',
574
+ THURSDAY = 'thursday',
575
+ FRIDAY = 'friday',
576
+ SATURDAY = 'saturday',
577
+ SUNDAY = 'sunday',
578
+ }
579
+
580
+ class DayOfWeek extends Enum {
581
+ protected getValues() {
582
+ return Object.values(EDaysOfWeek);
583
+ }
584
+ }
585
+ ```
586
+
587
+ #### Duration
588
+
589
+ Represents immutable duration values in milliseconds.
590
+
591
+ ```typescript
592
+ class Duration extends NumberValueObject {
593
+ constructor(value: number | NumberValueObject);
594
+
595
+ public static fromHours(hours: number): Duration;
596
+ public static fromMinutes(minutes: number): Duration;
597
+ public static fromSeconds(seconds: number): Duration;
598
+
599
+ public toHours(): number;
600
+ public toMinutes(): number;
601
+ public toSeconds(): number;
602
+ }
603
+ ```
604
+
605
+ #### Month
606
+
607
+ Represents immutable month enumeration (1-12).
608
+
609
+ ```typescript
610
+ class Month extends Enum {
611
+ protected getValues() {
612
+ return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
613
+ }
614
+ }
615
+ ```
616
+
617
+ #### MonthOfYear
618
+
619
+ Represents immutable month/year combination.
620
+
621
+ ```typescript
622
+ class MonthOfYear extends ValueObject<string> {
623
+ constructor(month: number | Month, year: number | Year);
624
+
625
+ public static fromTimestamp(timestamp: Timestamp): MonthOfYear;
626
+ public static fromString(value: string): MonthOfYear;
627
+
628
+ public getMonth(): Month;
629
+ public getYear(): Year;
630
+ public toString(): string; // Format: "YYYY/MM"
631
+ }
632
+ ```
633
+
634
+ #### Timestamp
635
+
636
+ Represents immutable timestamp values with comprehensive date/time operations.
637
+
638
+ ```typescript
639
+ class Timestamp extends ValueObject<number> {
640
+ constructor(value: number | Date | Timestamp | string);
641
+
642
+ // Static factory methods
643
+ public static now(): Timestamp;
644
+ public static fromDate(date: Date): Timestamp;
645
+
646
+ // Getters
647
+ public getYear(): Year;
648
+ public getMonth(): Month;
649
+ public getDay(): Day;
650
+ public getHours(): number;
651
+ public getMinutes(): number;
652
+ public getSeconds(): number;
653
+ public getDayOfWeek(): DayOfWeek;
654
+
655
+ // Arithmetic operations
656
+ public addDays(days: number): Timestamp;
657
+ public addHours(hours: number): Timestamp;
658
+ public addMinutes(minutes: number): Timestamp;
659
+ public addSeconds(seconds: number): Timestamp;
660
+
661
+ // Comparisons
662
+ public isBefore(other: Timestamp): boolean;
663
+ public isAfter(other: Timestamp): boolean;
664
+ public diffInDays(other: Timestamp): number;
665
+ public diffInHours(other: Timestamp): number;
666
+ public diffInMinutes(other: Timestamp): number;
667
+
668
+ // Conversions
669
+ public toDate(): Date;
670
+ public toISOString(): string;
671
+ }
672
+ ```
673
+
674
+ #### TimestampInterval
675
+
676
+ Represents immutable time intervals with start and end timestamps.
677
+
678
+ ```typescript
679
+ class TimestampInterval extends ValueObject<string> {
680
+ constructor(start: Timestamp, end: Timestamp);
681
+
682
+ public static fromPrimitives(primitives: PrimitiveOf<TimestampInterval>): TimestampInterval;
683
+
684
+ public getStart(): Timestamp;
685
+ public getEnd(): Timestamp;
686
+ public getDuration(): Duration;
687
+ public contains(timestamp: Timestamp): boolean;
688
+ public overlaps(other: TimestampInterval): boolean;
689
+ public split(parts: number): TimestampInterval[];
690
+ }
691
+ ```
692
+
693
+ ### Enum Value Objects
694
+
695
+ #### Enum
696
+
697
+ An abstract base class for creating type-safe enumeration value objects.
698
+
699
+ ```typescript
700
+ abstract class Enum<T extends Primitive = Primitive> extends ValueObject<T> {
701
+ constructor(protected readonly value: T);
702
+
703
+ public abstract getValues(): T[];
704
+ }
705
+ ```
706
+
707
+ **Features:**
708
+ - Type-safe enumeration validation
709
+ - Inheritance from ValueObject
710
+ - Static factory methods support
711
+ - Immutable enum values
712
+ - Comprehensive error handling
713
+
714
+ **Usage:**
715
+
716
+ ```typescript
717
+ import { Enum } from 'value-objects';
718
+
719
+ // Define your enum
720
+ enum UserStatus {
721
+ ACTIVE = 'active',
722
+ INACTIVE = 'inactive',
723
+ PENDING = 'pending',
724
+ SUSPENDED = 'suspended'
725
+ }
726
+
727
+ enum Priority {
728
+ LOW = 1,
729
+ MEDIUM = 2,
730
+ HIGH = 3,
731
+ URGENT = 4
732
+ }
733
+
734
+ // Create enum value object classes
735
+ class UserStatusEnum extends Enum {
736
+ public getValues() {
737
+ return Object.values(UserStatus);
738
+ }
739
+
740
+ // Static factory methods for convenience
741
+ static ACTIVE = () => new UserStatusEnum(UserStatus.ACTIVE);
742
+ static INACTIVE = () => new UserStatusEnum(UserStatus.INACTIVE);
743
+ static PENDING = () => new UserStatusEnum(UserStatus.PENDING);
744
+ static SUSPENDED = () => new UserStatusEnum(UserStatus.SUSPENDED);
745
+ }
746
+
747
+ class PriorityEnum extends Enum {
748
+ public getValues() {
749
+ return Object.values(Priority);
750
+ }
751
+
752
+ static LOW = () => new PriorityEnum(Priority.LOW);
753
+ static MEDIUM = () => new PriorityEnum(Priority.MEDIUM);
754
+ static HIGH = () => new PriorityEnum(Priority.HIGH);
755
+ static URGENT = () => new PriorityEnum(Priority.URGENT);
756
+ }
757
+
758
+ // Usage examples
759
+ const status = UserStatusEnum.ACTIVE();
760
+ const priority = new PriorityEnum(Priority.HIGH);
761
+
762
+ console.log(status.toString()); // 'active'
763
+ console.log(priority.valueOf()); // 3
764
+ console.log(status.isEqual('active')); // true
765
+ console.log(priority.isEqual(3)); // true
766
+
767
+ // Comparison
768
+ const status1 = UserStatusEnum.ACTIVE();
769
+ const status2 = new UserStatusEnum('active');
770
+ console.log(status1.isEqual(status2)); // true
771
+
772
+ // Clone
773
+ const statusCopy = (status as any).clone();
774
+ console.log(statusCopy.toString()); // 'active'
775
+
776
+ // Validation
777
+ try {
778
+ new UserStatusEnum('invalid'); // Throws ValueNotInEnumError
779
+ new PriorityEnum(999); // Throws ValueNotInEnumError
780
+ } catch (error) {
781
+ console.error('Invalid enum value');
782
+ }
783
+
784
+ // Mixed type enums
785
+ enum MixedEnum {
786
+ STRING_VALUE = 'text',
787
+ NUMBER_VALUE = 42
788
+ }
789
+
790
+ class MixedEnumValueObject extends Enum {
791
+ public getValues() {
792
+ return Object.values(MixedEnum);
793
+ }
794
+ }
795
+
796
+ const mixedString = new MixedEnumValueObject('text'); // Valid
797
+ const mixedNumber = new MixedEnumValueObject(42); // Valid
798
+ ```
799
+
800
+ ## 🚨 Error Handling
801
+
802
+ The library provides specific error types for different validation failures:
803
+
804
+ ```typescript
805
+ import {
806
+ InvalidStringLengthError,
807
+ InvalidNumberError,
808
+ InvalidIntegerError,
809
+ InvalidColorError,
810
+ InvalidHourError,
811
+ InvalidMinutesError,
812
+ InvalidEmailError,
813
+ InvalidPositiveNumberError,
814
+ ValueNotInEnumError,
815
+ InvalidDayError,
816
+ InvalidDayFormatError,
817
+ InvalidTimestampIntervalError,
818
+ NullObjectError
819
+ } from 'value-objects';
820
+
821
+ // String length validation
822
+ try {
823
+ new StringValueObject('too long string', 5);
824
+ } catch (error) {
825
+ if (error instanceof InvalidStringLengthError) {
826
+ console.error('String exceeds maximum length');
827
+ }
828
+ }
829
+
830
+ // Number validation
831
+ try {
832
+ new NumberValueObject(NaN);
833
+ } catch (error) {
834
+ if (error instanceof InvalidNumberError) {
835
+ console.error('Invalid number provided');
836
+ }
837
+ }
838
+
839
+ // Integer validation
840
+ try {
841
+ new Integer(42.5);
842
+ } catch (error) {
843
+ if (error instanceof InvalidIntegerError) {
844
+ console.error('Value must be a whole number');
845
+ }
846
+ }
847
+
848
+ try {
849
+ new Integer(Infinity);
850
+ } catch (error) {
851
+ if (error instanceof InvalidIntegerError) {
852
+ console.error('Infinity is not a valid integer');
853
+ }
854
+ }
855
+
856
+ // Color validation
857
+ try {
858
+ new Color('not-a-color');
859
+ } catch (error) {
860
+ if (error instanceof InvalidColorError) {
861
+ console.error('Invalid hex color format');
862
+ }
863
+ }
864
+
865
+ // Time validation
866
+ try {
867
+ new Hour('25:30'); // Invalid hour
868
+ } catch (error) {
869
+ if (error instanceof InvalidHourError) {
870
+ console.error('Hour must be between 0-23');
871
+ }
872
+ }
873
+
874
+ try {
875
+ new Hour(12, 75); // Invalid minutes
876
+ } catch (error) {
877
+ if (error instanceof InvalidMinutesError) {
878
+ console.error('Minutes must be between 0-59');
879
+ }
880
+ }
881
+
882
+ // Day validation
883
+ try {
884
+ new Day(32); // Invalid day
885
+ } catch (error) {
886
+ if (error instanceof InvalidDayError) {
887
+ console.error('Day must be between 1-31');
888
+ }
889
+ }
890
+
891
+ // Calendar day format validation
892
+ try {
893
+ new CalendarDay('invalid-date-format');
894
+ } catch (error) {
895
+ if (error instanceof InvalidDayFormatError) {
896
+ console.error('Invalid date format provided');
897
+ }
898
+ }
899
+
900
+ // Email validation
901
+ try {
902
+ new Email('invalid-email-format');
903
+ } catch (error) {
904
+ if (error instanceof InvalidEmailError) {
905
+ console.error('Invalid email format provided');
906
+ }
907
+ }
908
+
909
+ try {
910
+ new Email('user@domain.c'); // Domain extension too short
911
+ } catch (error) {
912
+ if (error instanceof InvalidEmailError) {
913
+ console.error('Email domain extension must be 2-13 characters');
914
+ }
915
+ }
916
+
917
+ // PositiveNumber validation
918
+ try {
919
+ new PositiveNumber(-5);
920
+ } catch (error) {
921
+ if (error instanceof InvalidPositiveNumberError) {
922
+ console.error('Value must be greater than 0');
923
+ }
924
+ }
925
+
926
+ try {
927
+ new PositiveNumber(0); // Zero is not positive
928
+ } catch (error) {
929
+ if (error instanceof InvalidPositiveNumberError) {
930
+ console.error('Zero is not a positive number');
931
+ }
932
+ }
933
+
934
+ // Enum validation
935
+ try {
936
+ new UserStatusEnum('invalid-status');
937
+ } catch (error) {
938
+ if (error instanceof ValueNotInEnumError) {
939
+ console.error('Value not found in enum: active,inactive,pending,suspended');
940
+ }
941
+ }
942
+
943
+ try {
944
+ new PriorityEnum(999); // Invalid number enum
945
+ } catch (error) {
946
+ if (error instanceof ValueNotInEnumError) {
947
+ console.error('Enum value must be one of: 1,2,3,4');
948
+ }
949
+ }
950
+
951
+ // Timestamp interval validation
952
+ try {
953
+ const start = new Timestamp(Date.now() + 1000);
954
+ const end = new Timestamp(Date.now());
955
+ new TimestampInterval(start, end); // Start after end
956
+ } catch (error) {
957
+ if (error instanceof InvalidTimestampIntervalError) {
958
+ console.error('Start timestamp must be before end timestamp');
959
+ }
960
+ }
961
+
962
+ // Null object error
963
+ try {
964
+ const nullValue = new StringValueObject(null);
965
+ nullValue.isEmpty(); // Throws NullObjectError
966
+ } catch (error) {
967
+ if (error instanceof NullObjectError) {
968
+ console.error('Cannot call method on null object');
969
+ }
970
+ }
971
+ ```
972
+
973
+ ## 🎯 Design Principles
974
+
975
+ ### Immutability
976
+
977
+ All value objects are immutable. Operations that appear to modify values actually return new instances:
978
+
979
+ ```typescript
980
+ const original = new NumberValueObject(10);
981
+ const modified = original.add(5);
982
+
983
+ console.log(original.valueOf()); // 10 (unchanged)
984
+ console.log(modified.valueOf()); // 15 (new instance)
985
+ ```
986
+
987
+ ### Value Equality
988
+
989
+ Value objects are compared by their values, not by reference:
990
+
991
+ ```typescript
992
+ const a = new StringValueObject('hello');
993
+ const b = new StringValueObject('hello');
994
+
995
+ console.log(a === b); // false (different instances)
996
+ console.log(a.isEqual(b)); // true (same value)
997
+ ```
998
+
999
+ ### Null Safety
1000
+
1001
+ The library properly handles null and undefined values using the Null Object pattern:
1002
+
1003
+ ```typescript
1004
+ const nullValue = new StringValueObject(undefined);
1005
+ console.log(NullObject.isNullObject(nullValue)); // true
1006
+ ```
1007
+
1008
+ If you try to call any method from a NullObject it will throw
1009
+ a NullObjectError:
1010
+
1011
+ ```typescript
1012
+ // NullObject StringValue instance
1013
+ const nullValue = new StringValueObject(undefined);
1014
+ nullValue.isEmpty(); // Throw NullObjectError!
1015
+ ```
1016
+
1017
+ This prevents defensive programming and if-else validations
1018
+
1019
+ ```typescript
1020
+ // Without NullObject Pattern
1021
+ let valueObject: StringValueObject;
1022
+ if (value) {
1023
+ valueObject = new StringValueObject(value);
1024
+ }
1025
+ else {
1026
+ throw Error('Invalid value');
1027
+ }
1028
+ return valueObject.isEmpty();
1029
+
1030
+ // With NullObject pattern
1031
+ valueObject = new StringValueObject(value);
1032
+ return valueObject.isEmpty();
1033
+ ```
1034
+
1035
+ ### Type Safety
1036
+
1037
+ The library provides full TypeScript support with strict typing to prevent runtime errors:
1038
+
1039
+ ```typescript
1040
+ // Compile-time type checking
1041
+ const amount: NumberValueObject = new NumberValueObject(100);
1042
+ const price: PositiveNumber = new PositiveNumber(29.99);
1043
+
1044
+ // Type inference works correctly
1045
+ const total = amount.add(price); // Type: NumberValueObject
1046
+ const isExpensive = price.isGreaterThan(50); // Type: boolean
1047
+ ```
1048
+
1049
+ ### Validation by Construction
1050
+
1051
+ All validation happens during object construction, ensuring that invalid states are impossible:
1052
+
1053
+ ```typescript
1054
+ // These will throw errors immediately
1055
+ new Email('invalid-email'); // InvalidEmailError
1056
+ new PositiveNumber(-1); // InvalidPositiveNumberError
1057
+ new Integer(3.14); // InvalidIntegerError
1058
+ new Color('not-a-color'); // InvalidColorError
1059
+ ```
1060
+
1061
+ ### Composition over Inheritance
1062
+
1063
+ Value objects can be composed together to create more complex domain models:
1064
+
1065
+ ```typescript
1066
+ class Product {
1067
+ constructor(
1068
+ private readonly name: StringValueObject,
1069
+ private readonly price: PositiveNumber,
1070
+ private readonly category: CategoryEnum
1071
+ ) {}
1072
+
1073
+ public getName(): StringValueObject {
1074
+ return this.name;
1075
+ }
1076
+
1077
+ public getPrice(): PositiveNumber {
1078
+ return this.price;
1079
+ }
1080
+
1081
+ public getCategory(): CategoryEnum {
1082
+ return this.category;
1083
+ }
1084
+ }
1085
+
1086
+ const product = new Product(
1087
+ new StringValueObject('Laptop'),
1088
+ new PositiveNumber(999.99),
1089
+ CategoryEnum.ELECTRONICS()
1090
+ );
1091
+ ```