@jsfsi-core/ts-crossplatform 1.1.3 → 1.1.5

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 +698 -0
  2. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsfsi-core/ts-crossplatform",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",