@jsfsi-core/ts-crossplatform 1.1.3 → 1.1.4
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/README.md +698 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
# @jsfsi-core/ts-crossplatform
|
|
2
|
+
|
|
3
|
+
Cross-platform TypeScript utilities for building robust applications with functional error handling, type-safe configuration, and common domain primitives.
|
|
4
|
+
|
|
5
|
+
## 📦 Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @jsfsi-core/ts-crossplatform
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 🏗️ Architecture
|
|
12
|
+
|
|
13
|
+
This package provides the **foundational building blocks** for the hexagonal architecture pattern:
|
|
14
|
+
|
|
15
|
+
- **Result Type**: Functional error handling without exceptions
|
|
16
|
+
- **Failure Classes**: Domain-specific error representations
|
|
17
|
+
- **Configuration**: Type-safe environment variable parsing
|
|
18
|
+
- **Domain Primitives**: Common utilities (GUID, DateTime, etc.)
|
|
19
|
+
|
|
20
|
+
These utilities are **framework-agnostic** and can be used in any TypeScript project (Node.js, NestJS, React, etc.).
|
|
21
|
+
|
|
22
|
+
## 📋 Features
|
|
23
|
+
|
|
24
|
+
### Result Type
|
|
25
|
+
|
|
26
|
+
Type-safe error handling using a tuple pattern:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Result, Ok, Fail } from '@jsfsi-core/ts-crossplatform';
|
|
30
|
+
|
|
31
|
+
type Result<T, E extends Failure> = [T, E | undefined];
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Example:**
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { Result, Ok, Fail, isFailure } from '@jsfsi-core/ts-crossplatform';
|
|
38
|
+
import { Failure } from '@jsfsi-core/ts-crossplatform';
|
|
39
|
+
|
|
40
|
+
class ValidationFailure extends Failure {
|
|
41
|
+
constructor(public readonly message: string) {
|
|
42
|
+
super();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateEmail(email: string): Result<string, ValidationFailure> {
|
|
47
|
+
if (!email.includes('@')) {
|
|
48
|
+
return Fail(new ValidationFailure('Invalid email format'));
|
|
49
|
+
}
|
|
50
|
+
return Ok(email);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Usage
|
|
54
|
+
const [email, failure] = validateEmail('user@example.com');
|
|
55
|
+
|
|
56
|
+
if (isFailure(ValidationFailure)(failure)) {
|
|
57
|
+
console.error(failure.message);
|
|
58
|
+
} else {
|
|
59
|
+
console.log('Valid email:', email);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Failure Classes
|
|
64
|
+
|
|
65
|
+
Base class for all domain failures:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { Failure } from '@jsfsi-core/ts-crossplatform';
|
|
69
|
+
|
|
70
|
+
export class SignInFailure extends Failure {
|
|
71
|
+
constructor(public readonly error: unknown) {
|
|
72
|
+
super();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Failure Matchers
|
|
78
|
+
|
|
79
|
+
**Always use `isFailure` and `notFailure` matchers** - never use `instanceof` directly:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { isFailure, notFailure } from '@jsfsi-core/ts-crossplatform';
|
|
83
|
+
|
|
84
|
+
const [user, failure] = await signIn();
|
|
85
|
+
|
|
86
|
+
// ✅ Correct way
|
|
87
|
+
if (isFailure(SignInFailure)(failure)) {
|
|
88
|
+
// TypeScript narrows type to SignInFailure
|
|
89
|
+
console.error('Sign in failed:', failure.error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (notFailure(SignInFailure)(failure)) {
|
|
93
|
+
// TypeScript knows it's not a SignInFailure
|
|
94
|
+
// Could be another failure type or undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ❌ Wrong way - Don't use instanceof
|
|
98
|
+
if (failure instanceof SignInFailure) {
|
|
99
|
+
// Avoid this pattern
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Configuration
|
|
104
|
+
|
|
105
|
+
Type-safe configuration parsing with Zod:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { z } from 'zod';
|
|
109
|
+
import { parseConfig } from '@jsfsi-core/ts-crossplatform';
|
|
110
|
+
|
|
111
|
+
const ConfigSchema = z.object({
|
|
112
|
+
PORT: z
|
|
113
|
+
.string()
|
|
114
|
+
.transform((val) => parseInt(val, 10))
|
|
115
|
+
.refine((val) => !isNaN(val), { message: 'PORT must be a valid number' }),
|
|
116
|
+
DATABASE_URL: z.string().url(),
|
|
117
|
+
ENABLE_LOGGING: z
|
|
118
|
+
.string()
|
|
119
|
+
.default('false')
|
|
120
|
+
.transform((val) => val.toLowerCase() === 'true'),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Throws if validation fails (use in application bootstrap)
|
|
124
|
+
export const config = parseConfig(ConfigSchema);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### GUID
|
|
128
|
+
|
|
129
|
+
Type-safe GUID generation:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { Guid } from '@jsfsi-core/ts-crossplatform';
|
|
133
|
+
|
|
134
|
+
const guid = Guid.newGuid();
|
|
135
|
+
console.log(guid); // e.g., "550e8400-e29b-41d4-a716-446655440000"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### DateTime
|
|
139
|
+
|
|
140
|
+
DateTime utilities for formatting dates and times, plus a promise-based sleep function:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { formatDate, formatTime, formatDateTime, sleep } from '@jsfsi-core/ts-crossplatform';
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### Sleep
|
|
147
|
+
|
|
148
|
+
Promise-based sleep function for async delays:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { sleep } from '@jsfsi-core/ts-crossplatform';
|
|
152
|
+
|
|
153
|
+
// Sleep for 1 second
|
|
154
|
+
await sleep(1000);
|
|
155
|
+
|
|
156
|
+
// Usage in async operations
|
|
157
|
+
async function processWithDelay() {
|
|
158
|
+
await sleep(500); // Wait 500ms
|
|
159
|
+
// Continue processing
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### Format Date
|
|
164
|
+
|
|
165
|
+
Format a timestamp as a date string (MM/DD/YYYY format):
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { formatDate } from '@jsfsi-core/ts-crossplatform';
|
|
169
|
+
|
|
170
|
+
const timestamp = new Date('2025-01-15').getTime();
|
|
171
|
+
const formatted = formatDate(timestamp);
|
|
172
|
+
console.log(formatted); // "01/15/2025"
|
|
173
|
+
|
|
174
|
+
// With locale support
|
|
175
|
+
const formattedDE = formatDate(timestamp, 'de-DE');
|
|
176
|
+
console.log(formattedDE); // "15.01.2025" (German format)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### Format Time
|
|
180
|
+
|
|
181
|
+
Format a timestamp as a time string (24-hour format HH:MM:SS):
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { formatTime } from '@jsfsi-core/ts-crossplatform';
|
|
185
|
+
|
|
186
|
+
const timestamp = new Date('2025-01-15T14:30:45').getTime();
|
|
187
|
+
const formatted = formatTime(timestamp);
|
|
188
|
+
console.log(formatted); // "14:30:45"
|
|
189
|
+
|
|
190
|
+
// With locale support
|
|
191
|
+
const formattedDE = formatTime(timestamp, 'de-DE');
|
|
192
|
+
console.log(formattedDE); // "14:30:45"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### Format Date and Time
|
|
196
|
+
|
|
197
|
+
Format a timestamp as both date and time:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { formatDateTime } from '@jsfsi-core/ts-crossplatform';
|
|
201
|
+
|
|
202
|
+
const timestamp = new Date('2025-01-15T14:30:45').getTime();
|
|
203
|
+
const formatted = formatDateTime(timestamp);
|
|
204
|
+
console.log(formatted); // "01/15/2025 14:30:45"
|
|
205
|
+
|
|
206
|
+
// With locale support
|
|
207
|
+
const formattedDE = formatDateTime(timestamp, 'de-DE');
|
|
208
|
+
console.log(formattedDE); // "15.01.2025 14:30:45" (German format)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Complete Example
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { formatDate, formatTime, formatDateTime, sleep } from '@jsfsi-core/ts-crossplatform';
|
|
215
|
+
|
|
216
|
+
// Format current date/time
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
console.log(formatDate(now)); // "01/15/2025"
|
|
219
|
+
console.log(formatTime(now)); // "14:30:45"
|
|
220
|
+
console.log(formatDateTime(now)); // "01/15/2025 14:30:45"
|
|
221
|
+
|
|
222
|
+
// Sleep in async function
|
|
223
|
+
async function delayedProcessing() {
|
|
224
|
+
console.log('Starting...');
|
|
225
|
+
await sleep(1000);
|
|
226
|
+
console.log('Done after 1 second');
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Notes:**
|
|
231
|
+
|
|
232
|
+
- All formatting functions accept a timestamp (number) as the first parameter
|
|
233
|
+
- Optional `locales` parameter for internationalization (follows Intl.LocalesArgument)
|
|
234
|
+
- Date format: `MM/DD/YYYY` (US format by default)
|
|
235
|
+
- Time format: `HH:MM:SS` (24-hour format, always)
|
|
236
|
+
- Sleep uses milliseconds (1 second = 1000ms)
|
|
237
|
+
|
|
238
|
+
### Partial Types
|
|
239
|
+
|
|
240
|
+
Recursive partial types for deep optional properties:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { RecursivePartial } from '@jsfsi-core/ts-crossplatform';
|
|
244
|
+
|
|
245
|
+
type User = {
|
|
246
|
+
id: string;
|
|
247
|
+
profile: {
|
|
248
|
+
name: string;
|
|
249
|
+
address: {
|
|
250
|
+
city: string;
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
type PartialUser = RecursivePartial<User>;
|
|
256
|
+
// All properties are optional, recursively
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Mock Utility
|
|
260
|
+
|
|
261
|
+
Type-safe mock utility for testing that works with `RecursivePartial` types:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { mock } from '@jsfsi-core/ts-crossplatform';
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### Basic Usage
|
|
268
|
+
|
|
269
|
+
Create mock objects with only the properties you need for testing:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { mock } from '@jsfsi-core/ts-crossplatform';
|
|
273
|
+
|
|
274
|
+
type User = {
|
|
275
|
+
id: string;
|
|
276
|
+
email: string;
|
|
277
|
+
name: string;
|
|
278
|
+
profile: {
|
|
279
|
+
bio: string;
|
|
280
|
+
avatar: string;
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Mock with no properties
|
|
285
|
+
const emptyUser = mock<User>();
|
|
286
|
+
// emptyUser is typed as User but with all properties undefined
|
|
287
|
+
|
|
288
|
+
// Mock with partial properties
|
|
289
|
+
const partialUser = mock<User>({
|
|
290
|
+
id: '123',
|
|
291
|
+
email: 'user@example.com',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Mock with nested properties
|
|
295
|
+
const fullUser = mock<User>({
|
|
296
|
+
id: '123',
|
|
297
|
+
email: 'user@example.com',
|
|
298
|
+
name: 'John Doe',
|
|
299
|
+
profile: {
|
|
300
|
+
bio: 'Software developer',
|
|
301
|
+
// avatar can be omitted - it's optional via RecursivePartial
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Testing Examples
|
|
307
|
+
|
|
308
|
+
Use mocks in your tests to create test data:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { describe, it, expect } from 'vitest';
|
|
312
|
+
import { mock } from '@jsfsi-core/ts-crossplatform';
|
|
313
|
+
|
|
314
|
+
describe('UserService', () => {
|
|
315
|
+
it('creates user with minimal data', async () => {
|
|
316
|
+
const userData = mock<User>({
|
|
317
|
+
email: 'test@example.com',
|
|
318
|
+
name: 'Test User',
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const [user, failure] = await userService.createUser(userData);
|
|
322
|
+
|
|
323
|
+
expect(user).toBeDefined();
|
|
324
|
+
expect(failure).toBeUndefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('handles user with nested profile', async () => {
|
|
328
|
+
const userData = mock<User>({
|
|
329
|
+
id: '123',
|
|
330
|
+
email: 'test@example.com',
|
|
331
|
+
profile: {
|
|
332
|
+
bio: 'Test bio',
|
|
333
|
+
// avatar omitted - RecursivePartial makes it optional
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const [user, failure] = await userService.updateUser(userData);
|
|
338
|
+
|
|
339
|
+
expect(user?.profile.bio).toBe('Test bio');
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### Complex Types
|
|
345
|
+
|
|
346
|
+
Works with complex nested types:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
import { mock } from '@jsfsi-core/ts-crossplatform';
|
|
350
|
+
|
|
351
|
+
type Order = {
|
|
352
|
+
id: string;
|
|
353
|
+
customer: {
|
|
354
|
+
id: string;
|
|
355
|
+
email: string;
|
|
356
|
+
address: {
|
|
357
|
+
street: string;
|
|
358
|
+
city: string;
|
|
359
|
+
zipCode: string;
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
items: Array<{
|
|
363
|
+
productId: string;
|
|
364
|
+
quantity: number;
|
|
365
|
+
price: number;
|
|
366
|
+
}>;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Mock with only what you need for the test
|
|
370
|
+
const orderMock = mock<Order>({
|
|
371
|
+
id: 'order-123',
|
|
372
|
+
customer: {
|
|
373
|
+
id: 'customer-456',
|
|
374
|
+
email: 'customer@example.com',
|
|
375
|
+
// address can be omitted - RecursivePartial makes it optional
|
|
376
|
+
},
|
|
377
|
+
items: [
|
|
378
|
+
{
|
|
379
|
+
productId: 'product-789',
|
|
380
|
+
quantity: 2,
|
|
381
|
+
// price can be omitted for this test
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### Benefits
|
|
388
|
+
|
|
389
|
+
- **Type Safety**: Mock objects are fully typed, catching errors at compile time
|
|
390
|
+
- **Flexibility**: Only specify the properties you need for each test
|
|
391
|
+
- **Recursive**: Works with deeply nested objects
|
|
392
|
+
- **Simple**: No complex setup or configuration required
|
|
393
|
+
|
|
394
|
+
## 📝 Naming Conventions
|
|
395
|
+
|
|
396
|
+
- **Result type**: Use `Result<T, E>` where `T` is success type, `E` extends `Failure`
|
|
397
|
+
- **Failure classes**: Suffix with `Failure` (e.g., `SignInFailure`, `ValidationFailure`)
|
|
398
|
+
- **Helper functions**: Use descriptive names (`Ok`, `Fail`, `isFailure`, `notFailure`)
|
|
399
|
+
|
|
400
|
+
## 🧪 Testing Principles
|
|
401
|
+
|
|
402
|
+
### Testing Result Types
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import { describe, it, expect } from 'vitest';
|
|
406
|
+
import { Ok, Fail, Result } from './result';
|
|
407
|
+
import { Failure, isFailure } from '../failures';
|
|
408
|
+
|
|
409
|
+
describe('validateEmail', () => {
|
|
410
|
+
it('returns Ok with email on valid input', () => {
|
|
411
|
+
const [email, failure] = validateEmail('user@example.com');
|
|
412
|
+
|
|
413
|
+
expect(email).toBe('user@example.com');
|
|
414
|
+
expect(failure).toBeUndefined();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('returns ValidationFailure on invalid input', () => {
|
|
418
|
+
const [email, failure] = validateEmail('invalid');
|
|
419
|
+
|
|
420
|
+
expect(email).toBeUndefined();
|
|
421
|
+
expect(isFailure(ValidationFailure)(failure)).toBe(true);
|
|
422
|
+
if (isFailure(ValidationFailure)(failure)) {
|
|
423
|
+
expect(failure.message).toBe('Invalid email format');
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Testing Failure Matchers
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
import { describe, it, expect } from 'vitest';
|
|
433
|
+
import { isFailure, notFailure } from './matchers';
|
|
434
|
+
import { Failure } from './failure';
|
|
435
|
+
|
|
436
|
+
class CustomFailure extends Failure {
|
|
437
|
+
constructor(public readonly message: string) {
|
|
438
|
+
super();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
describe('isFailure', () => {
|
|
443
|
+
it('matches when value is the failure type', () => {
|
|
444
|
+
const failure = new CustomFailure('error');
|
|
445
|
+
expect(isFailure(CustomFailure)(failure)).toBe(true);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('does not match when value is different failure type', () => {
|
|
449
|
+
const failure = new Failure();
|
|
450
|
+
expect(isFailure(CustomFailure)(failure)).toBe(false);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe('notFailure', () => {
|
|
455
|
+
it('matches when value is not the failure type', () => {
|
|
456
|
+
const failure = new Failure();
|
|
457
|
+
expect(notFailure(CustomFailure)(failure)).toBe(true);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
## ⚠️ Error Handling Principles
|
|
463
|
+
|
|
464
|
+
### Result Pattern
|
|
465
|
+
|
|
466
|
+
**Always use Result types for operations that can fail:**
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// ✅ Good
|
|
470
|
+
function parseNumber(input: string): Result<number, ParseFailure> {
|
|
471
|
+
const num = Number(input);
|
|
472
|
+
if (isNaN(num)) {
|
|
473
|
+
return Fail(new ParseFailure(`Cannot parse "${input}" as number`));
|
|
474
|
+
}
|
|
475
|
+
return Ok(num);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ❌ Bad - Throwing exceptions
|
|
479
|
+
function parseNumber(input: string): number {
|
|
480
|
+
const num = Number(input);
|
|
481
|
+
if (isNaN(num)) {
|
|
482
|
+
throw new Error(`Cannot parse "${input}" as number`);
|
|
483
|
+
}
|
|
484
|
+
return num;
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Failure Matchers
|
|
489
|
+
|
|
490
|
+
**Always use `isFailure` and `notFailure` matchers:**
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// ✅ Good
|
|
494
|
+
const [value, failure] = await operation();
|
|
495
|
+
if (isFailure(CustomFailure)(failure)) {
|
|
496
|
+
// Handle CustomFailure
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ❌ Bad
|
|
500
|
+
if (failure instanceof CustomFailure) {
|
|
501
|
+
// Don't use instanceof directly
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Chaining Results
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
function processUser(email: string): Result<User, ValidationFailure | SignInFailure> {
|
|
509
|
+
const [validEmail, emailFailure] = validateEmail(email);
|
|
510
|
+
|
|
511
|
+
if (isFailure(ValidationFailure)(emailFailure)) {
|
|
512
|
+
return Fail(emailFailure);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const [user, signInFailure] = await signIn(validEmail);
|
|
516
|
+
|
|
517
|
+
if (isFailure(SignInFailure)(signInFailure)) {
|
|
518
|
+
return Fail(signInFailure);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return Ok(user);
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## 🎯 Domain-Driven Design
|
|
526
|
+
|
|
527
|
+
### Failure as Domain Concept
|
|
528
|
+
|
|
529
|
+
Failures are part of your domain model:
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
// Domain failures represent business errors
|
|
533
|
+
export class SignInFailure extends Failure {
|
|
534
|
+
constructor(public readonly error: unknown) {
|
|
535
|
+
super();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export class InsufficientFundsFailure extends Failure {
|
|
540
|
+
constructor(
|
|
541
|
+
public readonly balance: number,
|
|
542
|
+
public readonly required: number,
|
|
543
|
+
) {
|
|
544
|
+
super();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Value Objects
|
|
550
|
+
|
|
551
|
+
Use Result types when returning value objects:
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
function createEmail(value: string): Result<Email, InvalidEmailFailure> {
|
|
555
|
+
if (!isValidEmail(value)) {
|
|
556
|
+
return Fail(new InvalidEmailFailure(value));
|
|
557
|
+
}
|
|
558
|
+
return Ok({ value } as Email);
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
## 🔄 Result Class Usage
|
|
563
|
+
|
|
564
|
+
### Basic Pattern
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
import { Result, Ok, Fail } from '@jsfsi-core/ts-crossplatform';
|
|
568
|
+
|
|
569
|
+
function divide(a: number, b: number): Result<number, DivisionByZeroFailure> {
|
|
570
|
+
if (b === 0) {
|
|
571
|
+
return Fail(new DivisionByZeroFailure());
|
|
572
|
+
}
|
|
573
|
+
return Ok(a / b);
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Handling Multiple Failure Types
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
type AuthResult = Result<User, SignInFailure | NetworkFailure>;
|
|
581
|
+
|
|
582
|
+
async function authenticate(email: string, password: string): Promise<AuthResult> {
|
|
583
|
+
const [networkCheck, networkFailure] = await checkNetwork();
|
|
584
|
+
|
|
585
|
+
if (isFailure(NetworkFailure)(networkFailure)) {
|
|
586
|
+
return Fail(networkFailure);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const [user, signInFailure] = await signIn(email, password);
|
|
590
|
+
|
|
591
|
+
if (isFailure(SignInFailure)(signInFailure)) {
|
|
592
|
+
return Fail(signInFailure);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return Ok(user);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Usage with type narrowing
|
|
599
|
+
const [user, failure] = await authenticate(email, password);
|
|
600
|
+
|
|
601
|
+
if (isFailure(SignInFailure)(failure)) {
|
|
602
|
+
// Handle sign-in failure
|
|
603
|
+
console.error('Sign in failed:', failure.error);
|
|
604
|
+
} else if (isFailure(NetworkFailure)(failure)) {
|
|
605
|
+
// Handle network failure
|
|
606
|
+
console.error('Network error:', failure.message);
|
|
607
|
+
} else {
|
|
608
|
+
// Success case
|
|
609
|
+
console.log('Authenticated:', user.name);
|
|
610
|
+
}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Early Returns Pattern
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
function processOrder(order: Order): Result<OrderId, ValidationFailure | PaymentFailure> {
|
|
617
|
+
// Early return on first failure
|
|
618
|
+
const [, validationFailure] = validateOrder(order);
|
|
619
|
+
if (isFailure(ValidationFailure)(validationFailure)) {
|
|
620
|
+
return Fail(validationFailure);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const [, paymentFailure] = processPayment(order);
|
|
624
|
+
if (isFailure(PaymentFailure)(paymentFailure)) {
|
|
625
|
+
return Fail(paymentFailure);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return Ok(order.id);
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## 📚 Best Practices
|
|
633
|
+
|
|
634
|
+
### 1. Type Safety
|
|
635
|
+
|
|
636
|
+
Always specify failure types explicitly:
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
// ✅ Good - Explicit failure types
|
|
640
|
+
function getUser(id: string): Result<User, UserNotFoundFailure | DatabaseFailure> {
|
|
641
|
+
// ...
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ⚠️ Acceptable - Generic Failure
|
|
645
|
+
function getUser(id: string): Result<User, Failure> {
|
|
646
|
+
// ...
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### 2. Failure Messages
|
|
651
|
+
|
|
652
|
+
Include meaningful information in failures:
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
// ✅ Good
|
|
656
|
+
export class ValidationFailure extends Failure {
|
|
657
|
+
constructor(
|
|
658
|
+
public readonly field: string,
|
|
659
|
+
public readonly message: string,
|
|
660
|
+
public readonly value: unknown,
|
|
661
|
+
) {
|
|
662
|
+
super();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ❌ Bad - No context
|
|
667
|
+
export class ValidationFailure extends Failure {
|
|
668
|
+
constructor() {
|
|
669
|
+
super();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### 3. Avoid Throwing
|
|
675
|
+
|
|
676
|
+
Never throw exceptions in domain logic - use Result types:
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
// ✅ Good
|
|
680
|
+
function parseConfig<T>(schema: z.ZodSchema<T>): Result<T, ConfigParseFailure> {
|
|
681
|
+
// ...
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ❌ Bad
|
|
685
|
+
function parseConfig<T>(schema: z.ZodSchema<T>): T {
|
|
686
|
+
// Throws exception
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
## 🔗 Additional Resources
|
|
691
|
+
|
|
692
|
+
- [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/)
|
|
693
|
+
- [Result Type Pattern](https://enterprisecraftsmanship.com/posts/functional-c-handling-failures-input-errors/)
|
|
694
|
+
- [Functional Error Handling](https://khalilstemmler.com/articles/enterprise-typescript-nodejs/handling-errors-result-type/)
|
|
695
|
+
|
|
696
|
+
## 📄 License
|
|
697
|
+
|
|
698
|
+
ISC
|