@haskou/value-objects 7.0.3 → 7.1.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.
- package/LICENSE.txt +1 -1
- package/README.md +213 -68
- package/package.json +8 -8
package/LICENSE.txt
CHANGED
package/README.md
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
# @haskou/value-objects
|
|
2
2
|
|
|
3
|
+
> Validated, immutable Value Objects for TypeScript and Domain-Driven Design.
|
|
4
|
+
|
|
5
|
+
Replace primitive obsession and defensive validation with domain-safe values that validate themselves.
|
|
6
|
+
|
|
3
7
|
[](https://github.com/haskou/value-objects/actions/workflows/ci.yml?query=branch%3Amaster)
|
|
4
8
|
[](https://codecov.io/gh/haskou/value-objects)
|
|
5
9
|
[](https://www.npmjs.com/package/@haskou/value-objects)
|
|
6
|
-
[](https://www.npmjs.com/package/@haskou/value-objects)
|
|
7
11
|
[](LICENSE.txt)
|
|
8
12
|
|
|
9
|
-
A TypeScript library for validated, immutable primitive wrappers and small utility value objects.
|
|
10
|
-
|
|
11
|
-
It provides ready-to-use objects for strings, numbers, identifiers, dates, coordinates, hashes, media, and collections. Validation happens when an object is created, so code receiving one of these objects can rely on its shape.
|
|
12
|
-
|
|
13
|
-
The package publishes ESM and CommonJS entry points plus TypeScript declarations. Both entry points share one runtime implementation, so class identity and NullObject configuration stay consistent when imports and requires are mixed. It does not declare a Node.js engine requirement. Identifier generation uses the installed `uuid` dependency with platform-specific Node and browser implementations.
|
|
14
|
-
|
|
15
|
-
## Documentation
|
|
16
|
-
|
|
17
|
-
Full documentation is available at **https://haskou.github.io/value-objects/**.
|
|
18
|
-
|
|
19
|
-
The documentation includes installation, quick start, examples, error handling, serialization notes, and one reference page per exported class.
|
|
20
|
-
|
|
21
13
|
## Installation
|
|
22
14
|
|
|
23
15
|
```bash
|
|
@@ -28,102 +20,255 @@ npm install @haskou/value-objects
|
|
|
28
20
|
yarn add @haskou/value-objects
|
|
29
21
|
```
|
|
30
22
|
|
|
31
|
-
##
|
|
23
|
+
## Why?
|
|
24
|
+
|
|
25
|
+
Primitive values force every consumer to defend itself.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
function sendEmail(email?: string | null) {
|
|
29
|
+
if (email === null || email === undefined) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (email.length === 0) {
|
|
34
|
+
throw new Error('Email cannot be empty');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!email.includes('@')) {
|
|
38
|
+
throw new Error('Invalid email');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// finally use the email
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
And another part of the application will eventually perform the same checks again, because `string` says nothing about what the value represents.
|
|
46
|
+
|
|
47
|
+
A Value Object moves those rules to the value itself:
|
|
32
48
|
|
|
33
49
|
```typescript
|
|
34
|
-
import {
|
|
50
|
+
import { Email } from '@haskou/value-objects';
|
|
35
51
|
|
|
36
52
|
const email = new Email('user@example.com');
|
|
37
|
-
const price = new PositiveNumber(29.99);
|
|
38
|
-
const color = new Color('#FF0000');
|
|
39
|
-
const hour = new Hour('09:30');
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.log(hour.addMinutes(45).toString()); // '10:15'
|
|
54
|
+
function sendEmail(email: Email) {
|
|
55
|
+
// already a valid Email
|
|
56
|
+
}
|
|
45
57
|
```
|
|
46
58
|
|
|
47
|
-
|
|
59
|
+
A non-null `Email` has already passed format validation, so consumers do not need to repeat those checks.
|
|
48
60
|
|
|
49
61
|
```typescript
|
|
50
|
-
|
|
62
|
+
new Email('not-an-email');
|
|
63
|
+
// throws InvalidEmailError
|
|
64
|
+
```
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
The same idea applies to numbers, identifiers, dates, coordinates, hashes and your own domain concepts.
|
|
67
|
+
|
|
68
|
+
Validation happens when values enter the domain instead of being repeated everywhere they are used.
|
|
69
|
+
|
|
70
|
+
## Build your own domain Value Objects
|
|
71
|
+
|
|
72
|
+
The built-in Value Objects are also intended to be extended.
|
|
73
|
+
|
|
74
|
+
If your domain has a `Username`, it should not remain a generic `string` with validation scattered around the application.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import {
|
|
78
|
+
assert,
|
|
79
|
+
NullObject,
|
|
80
|
+
StringValueObject,
|
|
81
|
+
} from '@haskou/value-objects';
|
|
82
|
+
|
|
83
|
+
export class Username extends StringValueObject {
|
|
84
|
+
constructor(value: string) {
|
|
85
|
+
super(value, 30);
|
|
86
|
+
|
|
87
|
+
if (NullObject.isNullObject(this)) return this;
|
|
88
|
+
|
|
89
|
+
assert(!this.isEmpty(), 'Username cannot be empty');
|
|
90
|
+
assert(value.length >= 3, 'Username must contain at least 3 characters');
|
|
91
|
+
assert(
|
|
92
|
+
/^[a-zA-Z0-9_]+$/.test(value),
|
|
93
|
+
'Username contains invalid characters',
|
|
94
|
+
);
|
|
57
95
|
}
|
|
58
96
|
}
|
|
59
97
|
```
|
|
60
98
|
|
|
61
|
-
|
|
99
|
+
Now the rules belong to the domain type:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const username = new Username('haskou');
|
|
103
|
+
|
|
104
|
+
function createUser(username: Username) {
|
|
105
|
+
// no need to validate the username again
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This is the main idea behind the library: use the provided Value Objects where they fit, and extend the base ones with your own domain invariants when they do not.
|
|
110
|
+
|
|
111
|
+
## Null without defensive programming
|
|
62
112
|
|
|
63
|
-
|
|
113
|
+
Missing values can be represented through the `NullObject` pattern instead of propagating `null | undefined` checks throughout the domain.
|
|
64
114
|
|
|
65
115
|
```typescript
|
|
66
|
-
import {
|
|
116
|
+
import {
|
|
117
|
+
NullObject,
|
|
118
|
+
NumberValueObject,
|
|
119
|
+
} from '@haskou/value-objects';
|
|
120
|
+
|
|
121
|
+
const limit = new NumberValueObject(undefined as never);
|
|
122
|
+
|
|
123
|
+
NullObject.isNullObject(limit);
|
|
124
|
+
// true
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The object can safely represent absence and pass through the domain without requiring:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
if (limit !== null && limit !== undefined) {
|
|
131
|
+
// ...
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
But a `NullObject` does not silently pretend to contain a valid value.
|
|
136
|
+
|
|
137
|
+
Calling behaviour that requires that value fails explicitly:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const limit = new NumberValueObject(undefined as never);
|
|
141
|
+
|
|
142
|
+
limit.isGreaterThan(10);
|
|
143
|
+
// throws NullObjectError
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This lets missing values travel through the same domain abstraction while still failing immediately when code tries to actually use them.
|
|
147
|
+
|
|
148
|
+
Applications that prefer strict construction can disable automatic `NullObject` creation:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import {
|
|
152
|
+
NumberValueObject,
|
|
153
|
+
ValueObject,
|
|
154
|
+
} from '@haskou/value-objects';
|
|
67
155
|
|
|
68
156
|
ValueObject.disableNullObjectCreation();
|
|
69
157
|
|
|
70
|
-
new
|
|
158
|
+
new NumberValueObject(undefined as never);
|
|
159
|
+
// throws NullObjectCreationDisabledError
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Assertions
|
|
71
163
|
|
|
72
|
-
|
|
164
|
+
The library exports an `assert` helper for expressing invariants without deeply nested validation branches.
|
|
165
|
+
|
|
166
|
+
Instead of:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
if (value !== undefined) {
|
|
170
|
+
if (value.length > 0) {
|
|
171
|
+
if (value.length <= 100) {
|
|
172
|
+
// ...
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
73
176
|
```
|
|
74
177
|
|
|
75
|
-
|
|
178
|
+
Use:
|
|
76
179
|
|
|
77
|
-
|
|
180
|
+
```typescript
|
|
181
|
+
import { assert } from '@haskou/value-objects';
|
|
78
182
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
| Numbers | `NumberValueObject`, `Integer`, `PositiveNumber` |
|
|
84
|
-
| IDs | `ShortId`, `UUID` |
|
|
85
|
-
| Time | `Timestamp`, `CalendarDay`, `Hour`, `Duration`, `MonthOfYear` |
|
|
86
|
-
| Coordinates | `Latitude`, `Longitude`, `Coordinates` |
|
|
87
|
-
| Hashes | `MD5Hash`, `SHA256Hash`, `SHA512Hash` |
|
|
88
|
-
| Media | `Media` |
|
|
89
|
-
| Collections | `UniqueObjectArray` |
|
|
183
|
+
assert(value !== undefined, 'Value is required');
|
|
184
|
+
assert(value.length > 0, 'Value cannot be empty');
|
|
185
|
+
assert(value.length <= 100, 'Value is too long');
|
|
186
|
+
```
|
|
90
187
|
|
|
91
|
-
|
|
188
|
+
Failed assertions throw the supplied error, or a `DomainError` when given a string.
|
|
92
189
|
|
|
93
|
-
|
|
190
|
+
Successful assertions use TypeScript assertion signatures, so the condition is narrowed afterwards.
|
|
94
191
|
|
|
95
|
-
|
|
96
|
-
# Start local docs server
|
|
97
|
-
yarn docs:dev
|
|
192
|
+
## Quick start
|
|
98
193
|
|
|
99
|
-
|
|
100
|
-
|
|
194
|
+
```typescript
|
|
195
|
+
import {
|
|
196
|
+
Color,
|
|
197
|
+
Email,
|
|
198
|
+
Hour,
|
|
199
|
+
PositiveNumber,
|
|
200
|
+
} from '@haskou/value-objects';
|
|
101
201
|
|
|
102
|
-
|
|
103
|
-
|
|
202
|
+
const email = new Email('user@example.com');
|
|
203
|
+
const price = new PositiveNumber(29.99);
|
|
204
|
+
const color = new Color('#FF0000');
|
|
205
|
+
const hour = new Hour('09:30');
|
|
206
|
+
|
|
207
|
+
email.valueOf(); // 'user@example.com'
|
|
208
|
+
price.valueOf(); // 29.99
|
|
209
|
+
color.toString(); // '#FF0000'
|
|
210
|
+
hour.addMinutes(45).toString(); // '10:15'
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Validation errors use specific error types:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import {
|
|
217
|
+
Email,
|
|
218
|
+
InvalidEmailError,
|
|
219
|
+
} from '@haskou/value-objects';
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
new Email('not-an-email');
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error instanceof InvalidEmailError) {
|
|
225
|
+
// Handle invalid domain input.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
104
228
|
```
|
|
105
229
|
|
|
106
|
-
##
|
|
230
|
+
## Available Value Objects
|
|
231
|
+
|
|
232
|
+
| Category | Examples |
|
|
233
|
+
| ----------- | ------------------------------------------------------------- |
|
|
234
|
+
| Base | `ValueObject`, `NullObject`, `Enum` |
|
|
235
|
+
| Strings | `StringValueObject`, `Password`, `Email`, `Color` |
|
|
236
|
+
| Numbers | `NumberValueObject`, `Integer`, `PositiveNumber` |
|
|
237
|
+
| Identifiers | `ShortId`, `UUID` |
|
|
238
|
+
| Date & time | `Timestamp`, `CalendarDay`, `Hour`, `Duration`, `MonthOfYear` |
|
|
239
|
+
| Coordinates | `Latitude`, `Longitude`, `Coordinates` |
|
|
240
|
+
| Hashes | `MD5Hash`, `SHA256Hash`, `SHA512Hash` |
|
|
241
|
+
| Media | `Media` |
|
|
242
|
+
| Collections | `UniqueObjectArray` |
|
|
243
|
+
|
|
244
|
+
The package is framework-agnostic and can be used in any TypeScript domain layer regardless of the framework, ORM or persistence technology around it.
|
|
245
|
+
|
|
246
|
+
## Documentation
|
|
247
|
+
|
|
248
|
+
Full documentation:
|
|
249
|
+
|
|
250
|
+
https://haskou.github.io/value-objects/
|
|
251
|
+
|
|
252
|
+
API reference:
|
|
253
|
+
|
|
254
|
+
https://haskou.github.io/value-objects/reference/
|
|
255
|
+
|
|
256
|
+
## Development
|
|
107
257
|
|
|
108
258
|
```bash
|
|
259
|
+
git clone https://github.com/haskou/value-objects.git
|
|
260
|
+
cd value-objects
|
|
261
|
+
|
|
109
262
|
yarn install
|
|
110
263
|
yarn test
|
|
111
264
|
yarn build
|
|
112
265
|
```
|
|
113
266
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
## Release branches
|
|
117
|
-
|
|
118
|
-
Publishing is handled by CI when a pull request is merged into the default branch.
|
|
267
|
+
## Contributing
|
|
119
268
|
|
|
120
|
-
|
|
121
|
-
| --- | --- |
|
|
122
|
-
| `fix/*` | Patch |
|
|
123
|
-
| `feat/*` | Minor |
|
|
124
|
-
| `break/*` | Major |
|
|
269
|
+
Bug reports, feature requests and pull requests are welcome.
|
|
125
270
|
|
|
126
|
-
|
|
271
|
+
https://github.com/haskou/value-objects/issues
|
|
127
272
|
|
|
128
273
|
## License
|
|
129
274
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haskou/value-objects",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.1",
|
|
4
4
|
"description": "TypeScript value objects for validated, immutable primitive values and small domain-safe data structures.",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -71,16 +71,16 @@
|
|
|
71
71
|
"@arethetypeswrong/core": "^0.18.5",
|
|
72
72
|
"@haskou/eslint-config": "^0.2.2",
|
|
73
73
|
"@types/jest": "^30.0.0",
|
|
74
|
-
"@types/node": "^26.6.
|
|
75
|
-
"eslint": "^10.
|
|
76
|
-
"jest": "^30.5.
|
|
74
|
+
"@types/node": "^26.6.4",
|
|
75
|
+
"eslint": "^10.11.0",
|
|
76
|
+
"jest": "^30.5.2",
|
|
77
77
|
"jest-extended": "^7.0.0",
|
|
78
|
-
"prettier": "^3.9.
|
|
79
|
-
"publint": "^0.3.
|
|
80
|
-
"ts-jest": "^29.4.
|
|
78
|
+
"prettier": "^3.9.9",
|
|
79
|
+
"publint": "^0.3.25",
|
|
80
|
+
"ts-jest": "^29.4.14",
|
|
81
81
|
"tsdown": "^0.23.0",
|
|
82
82
|
"typescript": "^6.0.3",
|
|
83
83
|
"vitepress": "^1.6.4",
|
|
84
|
-
"vue": "^3.5.
|
|
84
|
+
"vue": "^3.5.43"
|
|
85
85
|
}
|
|
86
86
|
}
|