@reasonabletech/utils 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2489 @@
1
+ # Utility Functions
2
+
3
+ This document describes the utility functions available in `@reasonabletech/utils` for common value checking, object construction, date/time handling, error handling, and retry patterns.
4
+
5
+ ## Overview
6
+
7
+ The utility functions provide clean, reusable ways to handle common patterns in TypeScript applications, especially when dealing with optional properties, `exactOptionalPropertyTypes: true`, Result-based error handling, and retry logic.
8
+
9
+ ## Table of Contents
10
+
11
+ - [String Functions](#string-functions)
12
+ - [Object Functions](#object-functions)
13
+ - [Date/Time Functions](#datetime-functions)
14
+ - [Result Type](#result-type)
15
+ - [Retry Functions](#retry-functions)
16
+
17
+ ## String Functions
18
+
19
+ ### `isEmptyString(value)`
20
+
21
+ Checks if a string value is empty, null, or undefined.
22
+
23
+ **Type Signature:**
24
+
25
+ ```typescript
26
+ function isEmptyString(
27
+ value: string | null | undefined,
28
+ ): value is null | undefined | "";
29
+ ```
30
+
31
+ **Parameters:**
32
+
33
+ - `value` - The string value to check
34
+
35
+ **Returns:**
36
+
37
+ - `true` if the value is `null`, `undefined`, or empty string `""`
38
+ - `false` otherwise
39
+
40
+ **Type Guard:**
41
+ This function acts as a type guard. When it returns `false`, TypeScript knows the value is a non-empty string.
42
+
43
+ **Examples:**
44
+
45
+ ```typescript
46
+ import { isEmptyString } from "@reasonabletech/utils";
47
+
48
+ // Basic usage
49
+ if (isEmptyString(someValue)) {
50
+ return null; // Handle empty case
51
+ }
52
+
53
+ // Type narrowing
54
+ const value: string | null | undefined = getUserInput();
55
+ if (!isEmptyString(value)) {
56
+ // TypeScript knows value is string here
57
+ console.log(value.toUpperCase());
58
+ }
59
+
60
+ // Replacing verbose checks
61
+ // ❌ Before:
62
+ if (base64Url === undefined || base64Url === null || base64Url === "") {
63
+ return null;
64
+ }
65
+
66
+ // ✅ After:
67
+ if (isEmptyString(base64Url)) {
68
+ return null;
69
+ }
70
+ ```
71
+
72
+ ---
73
+
74
+ ### `isNonEmptyString(value)`
75
+
76
+ Checks if a string is not empty and contains non-whitespace characters. This is a type guard version.
77
+
78
+ **Type Signature:**
79
+
80
+ ```typescript
81
+ function isNonEmptyString(value: string | null | undefined): value is string;
82
+ ```
83
+
84
+ **Parameters:**
85
+
86
+ - `value` - The string to check
87
+
88
+ **Returns:**
89
+
90
+ - `true` if the value is a non-empty string with non-whitespace content
91
+ - `false` for `null`, `undefined`, empty string, or whitespace-only string
92
+
93
+ **Type Guard:**
94
+ This function acts as a type guard. When it returns `true`, TypeScript knows the value is a string.
95
+
96
+ **Examples:**
97
+
98
+ ```typescript
99
+ import { isNonEmptyString } from "@reasonabletech/utils";
100
+
101
+ // Type narrowing
102
+ const input: string | null | undefined = getUserInput();
103
+ if (isNonEmptyString(input)) {
104
+ // TypeScript knows input is string here
105
+ processInput(input.trim());
106
+ }
107
+
108
+ // Filtering arrays
109
+ const values = ["hello", "", null, " ", "world"];
110
+ const nonEmpty = values.filter(isNonEmptyString);
111
+ // Result: ["hello", "world"]
112
+ ```
113
+
114
+ ---
115
+
116
+ ### `truncateString(str, maxLength)`
117
+
118
+ Truncates a string to a maximum length, adding ellipsis if truncated.
119
+
120
+ **Type Signature:**
121
+
122
+ ```typescript
123
+ function truncateString(str: string, maxLength: number): string;
124
+ ```
125
+
126
+ **Parameters:**
127
+
128
+ - `str` - The string to truncate
129
+ - `maxLength` - Maximum length including ellipsis (must be >= 3 for truncation to show ellipsis)
130
+
131
+ **Returns:**
132
+
133
+ - Original string if within maxLength
134
+ - Truncated string with "..." appended if exceeds maxLength
135
+
136
+ **Examples:**
137
+
138
+ ```typescript
139
+ import { truncateString } from "@reasonabletech/utils";
140
+
141
+ // Short string unchanged
142
+ truncateString("Hello", 10);
143
+ // Result: "Hello"
144
+
145
+ // Long string truncated
146
+ truncateString("Hello, World!", 10);
147
+ // Result: "Hello, ..."
148
+
149
+ // Useful for UI display
150
+ const displayName = truncateString(user.fullName, 20);
151
+ const preview = truncateString(article.content, 150);
152
+ ```
153
+
154
+ ---
155
+
156
+ ### `capitalize(str)`
157
+
158
+ Capitalizes the first letter of a string.
159
+
160
+ **Type Signature:**
161
+
162
+ ```typescript
163
+ function capitalize(str: string): string;
164
+ ```
165
+
166
+ **Parameters:**
167
+
168
+ - `str` - The string to capitalize
169
+
170
+ **Returns:**
171
+
172
+ - String with first letter capitalized, rest unchanged
173
+ - Empty string if input is empty
174
+
175
+ **Examples:**
176
+
177
+ ```typescript
178
+ import { capitalize } from "@reasonabletech/utils";
179
+
180
+ capitalize("hello");
181
+ // Result: "Hello"
182
+
183
+ capitalize("hello world");
184
+ // Result: "Hello world"
185
+
186
+ capitalize("");
187
+ // Result: ""
188
+
189
+ // Useful for display formatting
190
+ const label = capitalize(fieldName.replace(/_/g, " "));
191
+ ```
192
+
193
+ ---
194
+
195
+ ### `encodeBase64Url(data)`
196
+
197
+ Creates a base64url encoded string (URL-safe, no padding).
198
+
199
+ **Type Signature:**
200
+
201
+ ```typescript
202
+ function encodeBase64Url(data: string | Buffer): string;
203
+ ```
204
+
205
+ **Parameters:**
206
+
207
+ - `data` - Data to encode (string or Buffer)
208
+
209
+ **Returns:**
210
+
211
+ - Base64url encoded string with URL-safe characters (`-` instead of `+`, `_` instead of `/`) and no padding (`=`)
212
+
213
+ **Examples:**
214
+
215
+ ```typescript
216
+ import { encodeBase64Url } from "@reasonabletech/utils";
217
+
218
+ // Encode a string
219
+ encodeBase64Url("Hello, World!");
220
+ // Result: "SGVsbG8sIFdvcmxkIQ"
221
+
222
+ // Encode binary data
223
+ const buffer = Buffer.from([0x00, 0x01, 0x02, 0xff]);
224
+ encodeBase64Url(buffer);
225
+ // Result: "AAEC_w"
226
+
227
+ // Useful for JWT tokens, URL parameters
228
+ const encodedPayload = encodeBase64Url(JSON.stringify(payload));
229
+ ```
230
+
231
+ ---
232
+
233
+ ### `decodeBase64Url(encoded)`
234
+
235
+ Decodes a base64url encoded string.
236
+
237
+ **Type Signature:**
238
+
239
+ ```typescript
240
+ function decodeBase64Url(encoded: string): Buffer;
241
+ ```
242
+
243
+ **Parameters:**
244
+
245
+ - `encoded` - Base64url encoded string
246
+
247
+ **Returns:**
248
+
249
+ - Decoded data as Buffer
250
+
251
+ **Examples:**
252
+
253
+ ```typescript
254
+ import { decodeBase64Url } from "@reasonabletech/utils";
255
+
256
+ // Decode to string
257
+ const buffer = decodeBase64Url("SGVsbG8sIFdvcmxkIQ");
258
+ buffer.toString("utf-8");
259
+ // Result: "Hello, World!"
260
+
261
+ // Decode JWT payload
262
+ const payload = JSON.parse(decodeBase64Url(jwtPayloadPart).toString("utf-8"));
263
+ ```
264
+
265
+ ---
266
+
267
+ ### `isValidBase64Url(str)`
268
+
269
+ Checks if a string is in valid base64url format.
270
+
271
+ **Type Signature:**
272
+
273
+ ```typescript
274
+ function isValidBase64Url(str: string): boolean;
275
+ ```
276
+
277
+ **Parameters:**
278
+
279
+ - `str` - String to validate
280
+
281
+ **Returns:**
282
+
283
+ - `true` if the string only contains valid base64url characters (`A-Z`, `a-z`, `0-9`, `-`, `_`)
284
+ - `false` otherwise
285
+
286
+ **Examples:**
287
+
288
+ ```typescript
289
+ import { isValidBase64Url } from "@reasonabletech/utils";
290
+
291
+ isValidBase64Url("SGVsbG8"); // true
292
+ isValidBase64Url("SGVs+bG8"); // false (contains +)
293
+ isValidBase64Url("SGVs/bG8"); // false (contains /)
294
+ isValidBase64Url("SGVsbG8="); // false (contains =)
295
+
296
+ // Validate before decoding
297
+ if (isValidBase64Url(token)) {
298
+ const decoded = decodeBase64Url(token);
299
+ }
300
+ ```
301
+
302
+ ---
303
+
304
+ ### `getErrorMessage(error)` *(Deprecated)*
305
+
306
+ > **⚠️ Deprecated:** This utility is unnecessary. The logger accepts `ErrorLike` (unknown) directly via `logger.error(tag, message, error)`. Pass errors directly to the logger instead of manually converting to strings.
307
+
308
+ Extracts a message string from an unknown error value.
309
+
310
+ **Type Signature:**
311
+
312
+ ```typescript
313
+ function getErrorMessage(error: unknown): string;
314
+ ```
315
+
316
+ **Parameters:**
317
+
318
+ - `error` - Error value (Error object, string, or other)
319
+
320
+ **Returns:**
321
+
322
+ - Error message string extracted from the error
323
+
324
+ **Examples:**
325
+
326
+ ```typescript
327
+ // ❌ DEPRECATED: Manual error string extraction
328
+ try {
329
+ await operation();
330
+ } catch (error) {
331
+ logger.error("Component", getErrorMessage(error));
332
+ }
333
+
334
+ // ✅ CORRECT: Pass error directly to logger
335
+ try {
336
+ await operation();
337
+ } catch (error) {
338
+ logger.error("Component", "Operation failed", error);
339
+ }
340
+ ```
341
+
342
+ ---
343
+
344
+ ### `hasContent(value)`
345
+
346
+ Checks if a value has meaningful content (is a non-empty string).
347
+
348
+ **Type Signature:**
349
+
350
+ ```typescript
351
+ function hasContent(value: unknown): value is string;
352
+ ```
353
+
354
+ **Parameters:**
355
+
356
+ - `value` - The value to check (can be any type)
357
+
358
+ **Returns:**
359
+
360
+ - `true` if the value is a non-empty string
361
+ - `false` for `null`, `undefined`, empty string, or any non-string type
362
+
363
+ **Type Guard:**
364
+ This function acts as a type guard. When it returns `true`, TypeScript knows the value is a string.
365
+
366
+ **Examples:**
367
+
368
+ ```typescript
369
+ import { hasContent } from "@reasonabletech/utils";
370
+
371
+ // Type-safe content checking
372
+ const payload: Record<string, unknown> = getTokenPayload();
373
+
374
+ if (hasContent(payload.email)) {
375
+ // TypeScript knows payload.email is string here
376
+ sendEmailTo(payload.email);
377
+ }
378
+
379
+ // Filtering arrays
380
+ const values = [null, "", "hello", undefined, "world", 123];
381
+ const validStrings = values.filter(hasContent);
382
+ // validStrings: ["hello", "world"] (typed as string[])
383
+
384
+ // Object construction
385
+ const user = {
386
+ id: "123",
387
+ // Only include email if it has content
388
+ ...(hasContent(userData.email) ? { email: userData.email } : {}),
389
+ };
390
+ ```
391
+
392
+ ---
393
+
394
+ ### `withProperty(key, value)`
395
+
396
+ Creates an object with a property only if the value has content. This is particularly useful for building objects with optional properties while maintaining `exactOptionalPropertyTypes` compliance.
397
+
398
+ **Type Signature:**
399
+
400
+ ```typescript
401
+ function withProperty<K extends string>(
402
+ key: K,
403
+ value: unknown,
404
+ ): Record<K, string> | Record<string, never>;
405
+ ```
406
+
407
+ **Parameters:**
408
+
409
+ - `key` - The property key (must be a string literal type)
410
+ - `value` - The value to check
411
+
412
+ **Returns:**
413
+
414
+ - Object with the property if value has content: `{ [key]: value }`
415
+ - Empty object if value doesn't have content: `{}`
416
+
417
+ **Type Safety:**
418
+ The return type ensures proper TypeScript inference when used with object spread.
419
+
420
+ **Examples:**
421
+
422
+ ```typescript
423
+ import { withProperty } from "@reasonabletech/utils";
424
+
425
+ // Basic usage
426
+ const emailProp = withProperty("email", "user@example.com");
427
+ // Result: { email: "user@example.com" }
428
+
429
+ const emptyProp = withProperty("email", "");
430
+ // Result: {}
431
+
432
+ // Object construction with optional properties
433
+ const payload = {
434
+ sub: "user123",
435
+ email: "test@example.com",
436
+ name: "",
437
+ avatar: null,
438
+ };
439
+
440
+ const user = {
441
+ id: payload.sub as string,
442
+ ...withProperty("email", payload.email),
443
+ ...withProperty("name", payload.name),
444
+ ...withProperty("avatar", payload.avatar),
445
+ };
446
+ // Result: { id: "user123", email: "test@example.com" }
447
+
448
+ // Replacing verbose conditional spreads
449
+ // ❌ Before:
450
+ const user = {
451
+ id: "123",
452
+ ...(payload.email !== undefined &&
453
+ payload.email !== null &&
454
+ payload.email !== ""
455
+ ? { email: payload.email as string }
456
+ : {}),
457
+ ...(payload.name !== undefined && payload.name !== null && payload.name !== ""
458
+ ? { name: payload.name as string }
459
+ : {}),
460
+ };
461
+
462
+ // ✅ After:
463
+ const user = {
464
+ id: "123",
465
+ ...withProperty("email", payload.email),
466
+ ...withProperty("name", payload.name),
467
+ };
468
+ ```
469
+
470
+ ---
471
+
472
+ ## Object Functions
473
+
474
+ ### `includeIf(key, value)`
475
+
476
+ Conditionally includes a property in an object if the value is not undefined. This is useful for `exactOptionalPropertyTypes` compliance.
477
+
478
+ **Type Signature:**
479
+
480
+ ```typescript
481
+ function includeIf<K extends string, V>(
482
+ key: K,
483
+ value: V | undefined,
484
+ ): Record<string, unknown>;
485
+ ```
486
+
487
+ **Parameters:**
488
+
489
+ - `key` - The property key to conditionally include
490
+ - `value` - The value to include, or undefined to omit the property
491
+
492
+ **Returns:**
493
+
494
+ - Empty object `{}` if value is undefined
495
+ - Object `{ [key]: value }` if value is defined
496
+
497
+ **Examples:**
498
+
499
+ ```typescript
500
+ import { includeIf } from "@reasonabletech/utils";
501
+
502
+ // Basic usage
503
+ const obj = {
504
+ required: "value",
505
+ ...includeIf("optional", maybeUndefinedValue),
506
+ };
507
+ // If maybeUndefinedValue is "hello": { required: "value", optional: "hello" }
508
+ // If maybeUndefinedValue is undefined: { required: "value" }
509
+
510
+ // API response building
511
+ function createUserResponse(user: User) {
512
+ return {
513
+ id: user.id,
514
+ name: user.name,
515
+ ...includeIf("email", user.email),
516
+ ...includeIf("avatar", user.avatar?.url),
517
+ ...includeIf("lastLogin", user.lastLoginAt?.toISOString()),
518
+ };
519
+ }
520
+
521
+ // Billing service usage
522
+ return {
523
+ amount: pricing.amount,
524
+ ...includeIf("setupFee", pricing.setupFee),
525
+ ...includeIf("savings", calculatedSavings),
526
+ };
527
+ ```
528
+
529
+ ---
530
+
531
+ ### `includeIfDefined(obj)`
532
+
533
+ Conditionally includes multiple properties from an object, omitting any with undefined values.
534
+
535
+ **Type Signature:**
536
+
537
+ ```typescript
538
+ function includeIfDefined<T extends Record<string, unknown>>(
539
+ obj: T,
540
+ ): Record<string, unknown>;
541
+ ```
542
+
543
+ **Parameters:**
544
+
545
+ - `obj` - Object containing key-value pairs where values may be undefined
546
+
547
+ **Returns:**
548
+
549
+ - New object containing only the properties where values are not undefined
550
+
551
+ **Examples:**
552
+
553
+ ```typescript
554
+ import { includeIfDefined } from "@reasonabletech/utils";
555
+
556
+ // Basic usage
557
+ const obj = {
558
+ required: "value",
559
+ ...includeIfDefined({
560
+ optional1: maybeUndefined1,
561
+ optional2: maybeUndefined2,
562
+ }),
563
+ };
564
+
565
+ // Configuration object building
566
+ const config = {
567
+ host: "localhost",
568
+ port: 3000,
569
+ ...includeIfDefined({
570
+ ssl: sslEnabled ? sslConfig : undefined,
571
+ auth: authConfig,
572
+ timeout: userTimeout,
573
+ retries: retryCount,
574
+ }),
575
+ };
576
+
577
+ // Form data processing
578
+ const formData = {
579
+ name: data.name,
580
+ email: data.email,
581
+ ...includeIfDefined({
582
+ phone: data.phone?.trim() || undefined,
583
+ company: data.company?.trim() || undefined,
584
+ }),
585
+ };
586
+ ```
587
+
588
+ ---
589
+
590
+ ### `omitUndefined(obj)`
591
+
592
+ Omits properties with undefined values from an object. Alias for `includeIfDefined`.
593
+
594
+ **Type Signature:**
595
+
596
+ ```typescript
597
+ function omitUndefined<T extends Record<string, unknown>>(
598
+ obj: T,
599
+ ): Record<string, unknown>;
600
+ ```
601
+
602
+ **Parameters:**
603
+
604
+ - `obj` - The object to clean of undefined properties
605
+
606
+ **Returns:**
607
+
608
+ - New object with undefined properties removed (null, 0, "", false are preserved)
609
+
610
+ **Examples:**
611
+
612
+ ```typescript
613
+ import { omitUndefined } from "@reasonabletech/utils";
614
+
615
+ // Basic cleanup
616
+ const cleanObj = omitUndefined({
617
+ a: "defined",
618
+ b: undefined, // ❌ removed
619
+ c: null, // ✅ preserved (null ≠ undefined)
620
+ d: 0, // ✅ preserved (falsy but defined)
621
+ e: "", // ✅ preserved (falsy but defined)
622
+ f: false, // ✅ preserved (falsy but defined)
623
+ });
624
+ // Result: { a: "defined", c: null, d: 0, e: "", f: false }
625
+
626
+ // Before database save
627
+ const userUpdate = omitUndefined({
628
+ name: formData.name,
629
+ email: formData.email,
630
+ avatar: formData.avatar,
631
+ settings: formData.settings,
632
+ });
633
+ ```
634
+
635
+ ---
636
+
637
+ ### `conditionalProps(conditions)`
638
+
639
+ Creates an object with conditional properties based on boolean conditions.
640
+
641
+ **Type Signature:**
642
+
643
+ ```typescript
644
+ function conditionalProps(
645
+ conditions: Record<string, Record<string, unknown>>,
646
+ ): Record<string, unknown>;
647
+ ```
648
+
649
+ **Parameters:**
650
+
651
+ - `conditions` - Object where keys are stringified boolean conditions and values are objects to include
652
+
653
+ **Returns:**
654
+
655
+ - Object containing properties from all truthy conditions
656
+
657
+ **Examples:**
658
+
659
+ ```typescript
660
+ import { conditionalProps } from "@reasonabletech/utils";
661
+
662
+ // Basic conditional properties
663
+ const obj = {
664
+ always: "included",
665
+ ...conditionalProps({
666
+ [String(isEnabled)]: { feature: "enabled" },
667
+ [String(hasPermission)]: { admin: true },
668
+ }),
669
+ };
670
+
671
+ // Feature flags
672
+ const config = {
673
+ baseUrl: "https://api.example.com",
674
+ ...conditionalProps({
675
+ [String(enableLogging)]: { logging: { level: "debug" } },
676
+ [String(enableMetrics)]: { metrics: { endpoint: "/metrics" } },
677
+ [String(enableAuth)]: { auth: { provider: "oauth" } },
678
+ }),
679
+ };
680
+
681
+ // User permission-based UI
682
+ const uiConfig = {
683
+ showProfile: true,
684
+ ...conditionalProps({
685
+ [String(user.isAdmin)]: { showAdminPanel: true },
686
+ [String(user.isPremium)]: { showPremiumFeatures: true },
687
+ }),
688
+ };
689
+ ```
690
+
691
+ ---
692
+
693
+ ### `pick(obj, keys)`
694
+
695
+ Type-safe way to pick properties from an object.
696
+
697
+ **Type Signature:**
698
+
699
+ ```typescript
700
+ function pick<T extends Record<string, unknown>, K extends keyof T>(
701
+ obj: T,
702
+ keys: readonly K[],
703
+ ): Pick<T, K>;
704
+ ```
705
+
706
+ **Parameters:**
707
+
708
+ - `obj` - The source object to pick properties from
709
+ - `keys` - Array of keys to pick from the object
710
+
711
+ **Returns:**
712
+
713
+ - New object containing only the specified properties
714
+
715
+ **Examples:**
716
+
717
+ ```typescript
718
+ import { pick } from "@reasonabletech/utils";
719
+
720
+ // Basic property picking
721
+ const user = { id: 1, name: "John", email: "john@example.com", password: "secret" };
722
+ const publicUser = pick(user, ["id", "name", "email"]);
723
+ // Result: { id: 1, name: "John", email: "john@example.com" }
724
+
725
+ // API response filtering
726
+ function createPublicProfile(fullUser: FullUser) {
727
+ return pick(fullUser, ["id", "username", "displayName", "avatar", "joinedAt"]);
728
+ }
729
+
730
+ // Configuration subset
731
+ const fullConfig = { host: "localhost", port: 3000, ssl: true, debug: true, secret: "xxx" };
732
+ const clientConfig = pick(fullConfig, ["host", "port", "ssl"]);
733
+ // Result: { host: "localhost", port: 3000, ssl: true }
734
+ ```
735
+
736
+ ---
737
+
738
+ ### `omit(obj, keys)`
739
+
740
+ Type-safe way to omit properties from an object.
741
+
742
+ **Type Signature:**
743
+
744
+ ```typescript
745
+ function omit<T extends Record<string, unknown>, K extends keyof T>(
746
+ obj: T,
747
+ keys: readonly K[],
748
+ ): Omit<T, K>;
749
+ ```
750
+
751
+ **Parameters:**
752
+
753
+ - `obj` - The source object to omit properties from
754
+ - `keys` - Array of keys to omit from the object
755
+
756
+ **Returns:**
757
+
758
+ - New object with the specified properties removed
759
+
760
+ **Examples:**
761
+
762
+ ```typescript
763
+ import { omit } from "@reasonabletech/utils";
764
+
765
+ // Remove sensitive data
766
+ const user = { id: 1, name: "John", email: "john@example.com", password: "secret", ssn: "xxx" };
767
+ const safeUser = omit(user, ["password", "ssn"]);
768
+ // Result: { id: 1, name: "John", email: "john@example.com" }
769
+
770
+ // Remove internal properties
771
+ function toApiResponse(internalObj: InternalUser) {
772
+ return omit(internalObj, ["_id", "_version", "_internal", "hashedPassword", "secretKey"]);
773
+ }
774
+
775
+ // Configuration sanitization
776
+ const fullConfig = {
777
+ host: "localhost",
778
+ port: 3000,
779
+ apiSecret: "xxx",
780
+ dbPassword: "yyy",
781
+ };
782
+ const publicConfig = omit(fullConfig, ["apiSecret", "dbPassword"]);
783
+ // Result: { host: "localhost", port: 3000 }
784
+ ```
785
+
786
+ ---
787
+
788
+ ## Common Patterns
789
+
790
+ ### JWT Token Parsing
791
+
792
+ ```typescript
793
+ import { withProperty } from "@reasonabletech/utils";
794
+
795
+ function extractUserFromToken(token: string): {
796
+ id: string;
797
+ email?: string;
798
+ name?: string;
799
+ } | null {
800
+ const payload = decodeToken(token);
801
+ if (!payload) return null;
802
+
803
+ return {
804
+ id: (payload.sub as string) || "",
805
+ ...withProperty("email", payload.email),
806
+ ...withProperty("name", payload.name),
807
+ };
808
+ }
809
+ ```
810
+
811
+ ### API Response Processing
812
+
813
+ ```typescript
814
+ import { withProperty, hasContent } from "@reasonabletech/utils";
815
+
816
+ function processUserData(apiResponse: Record<string, unknown>) {
817
+ const user = {
818
+ id: apiResponse.id as string,
819
+ ...withProperty("email", apiResponse.email),
820
+ ...withProperty("displayName", apiResponse.display_name),
821
+ ...withProperty("avatar", apiResponse.avatar_url),
822
+ };
823
+
824
+ // Validate required fields
825
+ if (!hasContent(user.id)) {
826
+ throw new Error("User ID is required");
827
+ }
828
+
829
+ return user;
830
+ }
831
+ ```
832
+
833
+ ### Form Data Validation
834
+
835
+ ```typescript
836
+ import { hasContent, withProperty } from "@reasonabletech/utils";
837
+
838
+ function validateRegistrationForm(formData: FormData) {
839
+ const email = formData.get("email")?.toString();
840
+ const password = formData.get("password")?.toString();
841
+ const name = formData.get("name")?.toString();
842
+
843
+ const errors: string[] = [];
844
+
845
+ if (!hasContent(email)) {
846
+ errors.push("Email is required");
847
+ }
848
+
849
+ if (!hasContent(password)) {
850
+ errors.push("Password is required");
851
+ }
852
+
853
+ if (errors.length > 0) {
854
+ return { success: false, errors };
855
+ }
856
+
857
+ return {
858
+ success: true,
859
+ data: {
860
+ email: email as string, // TypeScript knows this is string
861
+ password: password as string,
862
+ ...withProperty("name", name), // Optional field
863
+ },
864
+ };
865
+ }
866
+ ```
867
+
868
+ ## Integration with exactOptionalPropertyTypes
869
+
870
+ These utilities are specifically designed to work well with TypeScript's `exactOptionalPropertyTypes: true` setting, which prevents assigning `undefined` to optional properties.
871
+
872
+ ```typescript
873
+ // ❌ This fails with exactOptionalPropertyTypes: true
874
+ interface User {
875
+ id: string;
876
+ email?: string;
877
+ }
878
+
879
+ const user: User = {
880
+ id: "123",
881
+ email: someValue, // Error if someValue could be undefined
882
+ };
883
+
884
+ // ✅ This works perfectly
885
+ const user: User = {
886
+ id: "123",
887
+ ...withProperty("email", someValue), // Only includes email if it has content
888
+ };
889
+ ```
890
+
891
+ ## Performance Considerations
892
+
893
+ - All functions are lightweight with minimal overhead
894
+ - `hasContent()` performs a simple type check and string comparison
895
+ - `withProperty()` creates objects conditionally, avoiding unnecessary object creation
896
+ - Functions are tree-shakeable when using modern bundlers
897
+
898
+ ## Modern Usage Refactors
899
+
900
+ ### Verbose Null Checks
901
+
902
+ ```typescript
903
+ // Before
904
+ if (value === null || value === undefined || value === "") {
905
+ // handle empty case
906
+ }
907
+
908
+ // After
909
+ if (isEmptyString(value)) {
910
+ // handle empty case
911
+ }
912
+ ```
913
+
914
+ ### Complex Conditional Spreads
915
+
916
+ ```typescript
917
+ // Before
918
+ const obj = {
919
+ required: "value",
920
+ ...(optional !== undefined && optional !== null && optional !== ""
921
+ ? { optional: optional as string }
922
+ : {}),
923
+ };
924
+
925
+ // After
926
+ const obj = {
927
+ required: "value",
928
+ ...withProperty("optional", optional),
929
+ };
930
+ ```
931
+
932
+ ### Manual Type Guards
933
+
934
+ ```typescript
935
+ // Before
936
+ function isValidString(value: unknown): value is string {
937
+ return typeof value === "string" && value !== "";
938
+ }
939
+
940
+ // After
941
+ import { hasContent } from "@reasonabletech/utils";
942
+ // Use hasContent directly
943
+ ```
944
+
945
+ ---
946
+
947
+ ## Date/Time Functions
948
+
949
+ This module provides standardized date/time handling utilities. Key principles:
950
+ - Use Date objects for all internal date/time representations
951
+ - Only convert to strings/numbers when required by external specs/APIs
952
+ - Provide clear, descriptive function names
953
+
954
+ ### `now()`
955
+
956
+ Gets the current Date object.
957
+
958
+ **Type Signature:**
959
+
960
+ ```typescript
961
+ function now(): Date;
962
+ ```
963
+
964
+ **Returns:**
965
+
966
+ - Current Date object
967
+
968
+ **Examples:**
969
+
970
+ ```typescript
971
+ import { now } from "@reasonabletech/utils";
972
+
973
+ const currentTime = now();
974
+ console.log(currentTime.toISOString());
975
+
976
+ // Use as a base for calculations
977
+ const tomorrow = addDays(now(), 1);
978
+ const lastWeek = subtractDays(now(), 7);
979
+ ```
980
+
981
+ ---
982
+
983
+ ### `dateToUnixTimestamp(date)`
984
+
985
+ Converts a Date object to Unix timestamp (seconds since epoch).
986
+
987
+ **Type Signature:**
988
+
989
+ ```typescript
990
+ function dateToUnixTimestamp(date: Date): number;
991
+ ```
992
+
993
+ **Parameters:**
994
+
995
+ - `date` - The Date object to convert
996
+
997
+ **Returns:**
998
+
999
+ - Unix timestamp in seconds
1000
+
1001
+ **Examples:**
1002
+
1003
+ ```typescript
1004
+ import { dateToUnixTimestamp, now } from "@reasonabletech/utils";
1005
+
1006
+ const timestamp = dateToUnixTimestamp(now());
1007
+ // Result: 1699876543 (example)
1008
+
1009
+ // Useful for JWT claims
1010
+ const jwtPayload = {
1011
+ sub: userId,
1012
+ iat: dateToUnixTimestamp(now()),
1013
+ exp: dateToUnixTimestamp(addHours(now(), 24)),
1014
+ };
1015
+ ```
1016
+
1017
+ ---
1018
+
1019
+ ### `unixTimestampToDate(timestamp)`
1020
+
1021
+ Converts a Unix timestamp (seconds since epoch) to a Date object.
1022
+
1023
+ **Type Signature:**
1024
+
1025
+ ```typescript
1026
+ function unixTimestampToDate(timestamp: number): Date;
1027
+ ```
1028
+
1029
+ **Parameters:**
1030
+
1031
+ - `timestamp` - Unix timestamp in seconds
1032
+
1033
+ **Returns:**
1034
+
1035
+ - Date object
1036
+
1037
+ **Examples:**
1038
+
1039
+ ```typescript
1040
+ import { unixTimestampToDate } from "@reasonabletech/utils";
1041
+
1042
+ const date = unixTimestampToDate(1699876543);
1043
+ console.log(date.toISOString());
1044
+ // Result: "2023-11-13T12:15:43.000Z" (example)
1045
+
1046
+ // Parse JWT expiration
1047
+ const expiresAt = unixTimestampToDate(jwtPayload.exp);
1048
+ if (isDateInPast(expiresAt)) {
1049
+ throw new Error("Token expired");
1050
+ }
1051
+ ```
1052
+
1053
+ ---
1054
+
1055
+ ### `dateToISOString(date)`
1056
+
1057
+ Converts a Date object to ISO string.
1058
+
1059
+ **Type Signature:**
1060
+
1061
+ ```typescript
1062
+ function dateToISOString(date: Date): string;
1063
+ ```
1064
+
1065
+ **Parameters:**
1066
+
1067
+ - `date` - The Date object to convert
1068
+
1069
+ **Returns:**
1070
+
1071
+ - ISO string representation (e.g., "2023-11-13T12:15:43.000Z")
1072
+
1073
+ **Examples:**
1074
+
1075
+ ```typescript
1076
+ import { dateToISOString, now } from "@reasonabletech/utils";
1077
+
1078
+ const isoString = dateToISOString(now());
1079
+ // Result: "2023-11-13T12:15:43.000Z"
1080
+
1081
+ // For API responses
1082
+ const response = {
1083
+ createdAt: dateToISOString(entity.createdAt),
1084
+ updatedAt: dateToISOString(entity.updatedAt),
1085
+ };
1086
+ ```
1087
+
1088
+ ---
1089
+
1090
+ ### `isoStringToDate(isoString)`
1091
+
1092
+ Converts an ISO string to a Date object.
1093
+
1094
+ **Type Signature:**
1095
+
1096
+ ```typescript
1097
+ function isoStringToDate(isoString: string): Date;
1098
+ ```
1099
+
1100
+ **Parameters:**
1101
+
1102
+ - `isoString` - ISO string representation
1103
+
1104
+ **Returns:**
1105
+
1106
+ - Date object
1107
+
1108
+ **Examples:**
1109
+
1110
+ ```typescript
1111
+ import { isoStringToDate } from "@reasonabletech/utils";
1112
+
1113
+ const date = isoStringToDate("2023-11-13T12:15:43.000Z");
1114
+
1115
+ // Parse API response dates
1116
+ const createdAt = isoStringToDate(apiResponse.created_at);
1117
+ ```
1118
+
1119
+ ---
1120
+
1121
+ ### `normalizeToDate(dateOrString)`
1122
+
1123
+ Converts a Date | string union to a Date object. Useful for migration from mixed patterns.
1124
+
1125
+ **Type Signature:**
1126
+
1127
+ ```typescript
1128
+ function normalizeToDate(dateOrString: Date | string): Date;
1129
+ ```
1130
+
1131
+ **Parameters:**
1132
+
1133
+ - `dateOrString` - Date object or ISO string
1134
+
1135
+ **Returns:**
1136
+
1137
+ - Date object (returns input directly if already a Date)
1138
+
1139
+ **Examples:**
1140
+
1141
+ ```typescript
1142
+ import { normalizeToDate } from "@reasonabletech/utils";
1143
+
1144
+ // Handles both Date and string inputs
1145
+ const date1 = normalizeToDate(new Date());
1146
+ const date2 = normalizeToDate("2023-11-13T12:15:43.000Z");
1147
+
1148
+ // Useful when migrating APIs
1149
+ function processEvent(event: { timestamp: Date | string }) {
1150
+ const timestamp = normalizeToDate(event.timestamp);
1151
+ // Now always a Date object
1152
+ }
1153
+ ```
1154
+
1155
+ ---
1156
+
1157
+ ### `isDateInPast(date)`
1158
+
1159
+ Checks if a Date object represents a time in the past.
1160
+
1161
+ **Type Signature:**
1162
+
1163
+ ```typescript
1164
+ function isDateInPast(date: Date): boolean;
1165
+ ```
1166
+
1167
+ **Parameters:**
1168
+
1169
+ - `date` - The Date object to check
1170
+
1171
+ **Returns:**
1172
+
1173
+ - `true` if the date is in the past
1174
+
1175
+ **Examples:**
1176
+
1177
+ ```typescript
1178
+ import { isDateInPast, subtractDays, now } from "@reasonabletech/utils";
1179
+
1180
+ const yesterday = subtractDays(now(), 1);
1181
+ isDateInPast(yesterday); // true
1182
+
1183
+ const tomorrow = addDays(now(), 1);
1184
+ isDateInPast(tomorrow); // false
1185
+
1186
+ // Token validation
1187
+ if (isDateInPast(tokenExpiresAt)) {
1188
+ throw new TokenExpiredError();
1189
+ }
1190
+ ```
1191
+
1192
+ ---
1193
+
1194
+ ### `isDateInFuture(date)`
1195
+
1196
+ Checks if a Date object represents a time in the future.
1197
+
1198
+ **Type Signature:**
1199
+
1200
+ ```typescript
1201
+ function isDateInFuture(date: Date): boolean;
1202
+ ```
1203
+
1204
+ **Parameters:**
1205
+
1206
+ - `date` - The Date object to check
1207
+
1208
+ **Returns:**
1209
+
1210
+ - `true` if the date is in the future
1211
+
1212
+ **Examples:**
1213
+
1214
+ ```typescript
1215
+ import { isDateInFuture, addDays, now } from "@reasonabletech/utils";
1216
+
1217
+ const tomorrow = addDays(now(), 1);
1218
+ isDateInFuture(tomorrow); // true
1219
+
1220
+ const yesterday = subtractDays(now(), 1);
1221
+ isDateInFuture(yesterday); // false
1222
+
1223
+ // Scheduling validation
1224
+ if (!isDateInFuture(scheduledAt)) {
1225
+ throw new Error("Scheduled time must be in the future");
1226
+ }
1227
+ ```
1228
+
1229
+ ---
1230
+
1231
+ ### `addSeconds(date, seconds)`
1232
+
1233
+ Adds seconds to a Date object.
1234
+
1235
+ **Type Signature:**
1236
+
1237
+ ```typescript
1238
+ function addSeconds(date: Date, seconds: number): Date;
1239
+ ```
1240
+
1241
+ **Parameters:**
1242
+
1243
+ - `date` - The base Date object
1244
+ - `seconds` - Number of seconds to add
1245
+
1246
+ **Returns:**
1247
+
1248
+ - New Date object with added seconds
1249
+
1250
+ **Examples:**
1251
+
1252
+ ```typescript
1253
+ import { addSeconds, now } from "@reasonabletech/utils";
1254
+
1255
+ const later = addSeconds(now(), 30);
1256
+ // 30 seconds from now
1257
+
1258
+ // Token with short lifetime
1259
+ const tokenExpiry = addSeconds(now(), 300); // 5 minutes
1260
+ ```
1261
+
1262
+ ---
1263
+
1264
+ ### `subtractSeconds(date, seconds)`
1265
+
1266
+ Subtracts seconds from a Date object.
1267
+
1268
+ **Type Signature:**
1269
+
1270
+ ```typescript
1271
+ function subtractSeconds(date: Date, seconds: number): Date;
1272
+ ```
1273
+
1274
+ **Parameters:**
1275
+
1276
+ - `date` - The base Date object
1277
+ - `seconds` - Number of seconds to subtract
1278
+
1279
+ **Returns:**
1280
+
1281
+ - New Date object with subtracted seconds
1282
+
1283
+ **Examples:**
1284
+
1285
+ ```typescript
1286
+ import { subtractSeconds, now } from "@reasonabletech/utils";
1287
+
1288
+ const earlier = subtractSeconds(now(), 60);
1289
+ // 1 minute ago
1290
+ ```
1291
+
1292
+ ---
1293
+
1294
+ ### `addMinutes(date, minutes)`
1295
+
1296
+ Adds minutes to a Date object.
1297
+
1298
+ **Type Signature:**
1299
+
1300
+ ```typescript
1301
+ function addMinutes(date: Date, minutes: number): Date;
1302
+ ```
1303
+
1304
+ **Parameters:**
1305
+
1306
+ - `date` - The base Date object
1307
+ - `minutes` - Number of minutes to add
1308
+
1309
+ **Returns:**
1310
+
1311
+ - New Date object with added minutes
1312
+
1313
+ **Examples:**
1314
+
1315
+ ```typescript
1316
+ import { addMinutes, now } from "@reasonabletech/utils";
1317
+
1318
+ const meetingEnd = addMinutes(meetingStart, 60);
1319
+
1320
+ // Session timeout
1321
+ const sessionExpiry = addMinutes(now(), 30);
1322
+ ```
1323
+
1324
+ ---
1325
+
1326
+ ### `subtractMinutes(date, minutes)`
1327
+
1328
+ Subtracts minutes from a Date object.
1329
+
1330
+ **Type Signature:**
1331
+
1332
+ ```typescript
1333
+ function subtractMinutes(date: Date, minutes: number): Date;
1334
+ ```
1335
+
1336
+ **Parameters:**
1337
+
1338
+ - `date` - The base Date object
1339
+ - `minutes` - Number of minutes to subtract
1340
+
1341
+ **Returns:**
1342
+
1343
+ - New Date object with subtracted minutes
1344
+
1345
+ **Examples:**
1346
+
1347
+ ```typescript
1348
+ import { subtractMinutes, now } from "@reasonabletech/utils";
1349
+
1350
+ const gracePeriodStart = subtractMinutes(deadline, 15);
1351
+ ```
1352
+
1353
+ ---
1354
+
1355
+ ### `addHours(date, hours)`
1356
+
1357
+ Adds hours to a Date object.
1358
+
1359
+ **Type Signature:**
1360
+
1361
+ ```typescript
1362
+ function addHours(date: Date, hours: number): Date;
1363
+ ```
1364
+
1365
+ **Parameters:**
1366
+
1367
+ - `date` - The base Date object
1368
+ - `hours` - Number of hours to add
1369
+
1370
+ **Returns:**
1371
+
1372
+ - New Date object with added hours
1373
+
1374
+ **Examples:**
1375
+
1376
+ ```typescript
1377
+ import { addHours, now } from "@reasonabletech/utils";
1378
+
1379
+ const tokenExpiry = addHours(now(), 24);
1380
+ // Token valid for 24 hours
1381
+
1382
+ const nextShift = addHours(shiftStart, 8);
1383
+ ```
1384
+
1385
+ ---
1386
+
1387
+ ### `subtractHours(date, hours)`
1388
+
1389
+ Subtracts hours from a Date object.
1390
+
1391
+ **Type Signature:**
1392
+
1393
+ ```typescript
1394
+ function subtractHours(date: Date, hours: number): Date;
1395
+ ```
1396
+
1397
+ **Parameters:**
1398
+
1399
+ - `date` - The base Date object
1400
+ - `hours` - Number of hours to subtract
1401
+
1402
+ **Returns:**
1403
+
1404
+ - New Date object with subtracted hours
1405
+
1406
+ **Examples:**
1407
+
1408
+ ```typescript
1409
+ import { subtractHours, now } from "@reasonabletech/utils";
1410
+
1411
+ const startTime = subtractHours(now(), 2);
1412
+ // 2 hours ago
1413
+ ```
1414
+
1415
+ ---
1416
+
1417
+ ### `addDays(date, days)`
1418
+
1419
+ Adds days to a Date object.
1420
+
1421
+ **Type Signature:**
1422
+
1423
+ ```typescript
1424
+ function addDays(date: Date, days: number): Date;
1425
+ ```
1426
+
1427
+ **Parameters:**
1428
+
1429
+ - `date` - The base Date object
1430
+ - `days` - Number of days to add
1431
+
1432
+ **Returns:**
1433
+
1434
+ - New Date object with added days
1435
+
1436
+ **Examples:**
1437
+
1438
+ ```typescript
1439
+ import { addDays, now } from "@reasonabletech/utils";
1440
+
1441
+ const nextWeek = addDays(now(), 7);
1442
+ const trialEnd = addDays(signupDate, 14);
1443
+
1444
+ // Calculate due date
1445
+ const dueDate = addDays(invoiceDate, 30);
1446
+ ```
1447
+
1448
+ ---
1449
+
1450
+ ### `subtractDays(date, days)`
1451
+
1452
+ Subtracts days from a Date object.
1453
+
1454
+ **Type Signature:**
1455
+
1456
+ ```typescript
1457
+ function subtractDays(date: Date, days: number): Date;
1458
+ ```
1459
+
1460
+ **Parameters:**
1461
+
1462
+ - `date` - The base Date object
1463
+ - `days` - Number of days to subtract
1464
+
1465
+ **Returns:**
1466
+
1467
+ - New Date object with subtracted days
1468
+
1469
+ **Examples:**
1470
+
1471
+ ```typescript
1472
+ import { subtractDays, now } from "@reasonabletech/utils";
1473
+
1474
+ const lastWeek = subtractDays(now(), 7);
1475
+
1476
+ // Get records from last 30 days
1477
+ const startDate = subtractDays(now(), 30);
1478
+ ```
1479
+
1480
+ ---
1481
+
1482
+ ### `diffInSeconds(laterDate, earlierDate)`
1483
+
1484
+ Calculates the difference between two dates in seconds.
1485
+
1486
+ **Type Signature:**
1487
+
1488
+ ```typescript
1489
+ function diffInSeconds(laterDate: Date, earlierDate: Date): number;
1490
+ ```
1491
+
1492
+ **Parameters:**
1493
+
1494
+ - `laterDate` - The later date
1495
+ - `earlierDate` - The earlier date
1496
+
1497
+ **Returns:**
1498
+
1499
+ - Difference in seconds (positive if laterDate is after earlierDate)
1500
+
1501
+ **Examples:**
1502
+
1503
+ ```typescript
1504
+ import { diffInSeconds, now, addMinutes } from "@reasonabletech/utils";
1505
+
1506
+ const start = now();
1507
+ const end = addMinutes(start, 5);
1508
+ diffInSeconds(end, start); // 300
1509
+
1510
+ // Calculate elapsed time
1511
+ const elapsedSeconds = diffInSeconds(now(), startTime);
1512
+ ```
1513
+
1514
+ ---
1515
+
1516
+ ### `diffInMinutes(laterDate, earlierDate)`
1517
+
1518
+ Calculates the difference between two dates in minutes.
1519
+
1520
+ **Type Signature:**
1521
+
1522
+ ```typescript
1523
+ function diffInMinutes(laterDate: Date, earlierDate: Date): number;
1524
+ ```
1525
+
1526
+ **Parameters:**
1527
+
1528
+ - `laterDate` - The later date
1529
+ - `earlierDate` - The earlier date
1530
+
1531
+ **Returns:**
1532
+
1533
+ - Difference in minutes (positive if laterDate is after earlierDate)
1534
+
1535
+ **Examples:**
1536
+
1537
+ ```typescript
1538
+ import { diffInMinutes, now } from "@reasonabletech/utils";
1539
+
1540
+ const minutesElapsed = diffInMinutes(now(), sessionStart);
1541
+ if (minutesElapsed > 30) {
1542
+ // Session timeout
1543
+ }
1544
+ ```
1545
+
1546
+ ---
1547
+
1548
+ ### `diffInHours(laterDate, earlierDate)`
1549
+
1550
+ Calculates the difference between two dates in hours.
1551
+
1552
+ **Type Signature:**
1553
+
1554
+ ```typescript
1555
+ function diffInHours(laterDate: Date, earlierDate: Date): number;
1556
+ ```
1557
+
1558
+ **Parameters:**
1559
+
1560
+ - `laterDate` - The later date
1561
+ - `earlierDate` - The earlier date
1562
+
1563
+ **Returns:**
1564
+
1565
+ - Difference in hours (positive if laterDate is after earlierDate)
1566
+
1567
+ **Examples:**
1568
+
1569
+ ```typescript
1570
+ import { diffInHours, now } from "@reasonabletech/utils";
1571
+
1572
+ const hoursAgo = diffInHours(now(), lastActivity);
1573
+ if (hoursAgo > 24) {
1574
+ // More than a day since last activity
1575
+ }
1576
+ ```
1577
+
1578
+ ---
1579
+
1580
+ ### `diffInDays(laterDate, earlierDate)`
1581
+
1582
+ Calculates the difference between two dates in days.
1583
+
1584
+ **Type Signature:**
1585
+
1586
+ ```typescript
1587
+ function diffInDays(laterDate: Date, earlierDate: Date): number;
1588
+ ```
1589
+
1590
+ **Parameters:**
1591
+
1592
+ - `laterDate` - The later date
1593
+ - `earlierDate` - The earlier date
1594
+
1595
+ **Returns:**
1596
+
1597
+ - Difference in days (positive if laterDate is after earlierDate)
1598
+
1599
+ **Examples:**
1600
+
1601
+ ```typescript
1602
+ import { diffInDays, now } from "@reasonabletech/utils";
1603
+
1604
+ const daysRemaining = diffInDays(subscriptionEnd, now());
1605
+ if (daysRemaining <= 7) {
1606
+ // Send renewal reminder
1607
+ }
1608
+
1609
+ const accountAge = diffInDays(now(), user.createdAt);
1610
+ ```
1611
+
1612
+ ---
1613
+
1614
+ ### `isSameDay(date1, date2)`
1615
+
1616
+ Checks if two dates represent the same calendar day (in UTC).
1617
+
1618
+ **Type Signature:**
1619
+
1620
+ ```typescript
1621
+ function isSameDay(date1: Date, date2: Date): boolean;
1622
+ ```
1623
+
1624
+ **Parameters:**
1625
+
1626
+ - `date1` - First date
1627
+ - `date2` - Second date
1628
+
1629
+ **Returns:**
1630
+
1631
+ - `true` if both dates represent the same calendar day in UTC
1632
+
1633
+ **Examples:**
1634
+
1635
+ ```typescript
1636
+ import { isSameDay, now, addHours } from "@reasonabletech/utils";
1637
+
1638
+ const morning = new Date("2023-11-13T08:00:00Z");
1639
+ const evening = new Date("2023-11-13T20:00:00Z");
1640
+ isSameDay(morning, evening); // true
1641
+
1642
+ const today = now();
1643
+ const tomorrow = addDays(today, 1);
1644
+ isSameDay(today, tomorrow); // false
1645
+
1646
+ // Group events by day
1647
+ if (isSameDay(event.date, selectedDate)) {
1648
+ dayEvents.push(event);
1649
+ }
1650
+ ```
1651
+
1652
+ ---
1653
+
1654
+ ### `formatDateISO(date)`
1655
+
1656
+ Formats a date as YYYY-MM-DD.
1657
+
1658
+ **Type Signature:**
1659
+
1660
+ ```typescript
1661
+ function formatDateISO(date: Date): string;
1662
+ ```
1663
+
1664
+ **Parameters:**
1665
+
1666
+ - `date` - The date to format
1667
+
1668
+ **Returns:**
1669
+
1670
+ - Date string in YYYY-MM-DD format
1671
+
1672
+ **Examples:**
1673
+
1674
+ ```typescript
1675
+ import { formatDateISO, now } from "@reasonabletech/utils";
1676
+
1677
+ formatDateISO(now());
1678
+ // Result: "2023-11-13"
1679
+
1680
+ // For date inputs
1681
+ const dateInput = formatDateISO(selectedDate);
1682
+ // <input type="date" value={dateInput} />
1683
+
1684
+ // File naming
1685
+ const filename = `report-${formatDateISO(now())}.csv`;
1686
+ ```
1687
+
1688
+ ---
1689
+
1690
+ ### `formatTimeISO(date)`
1691
+
1692
+ Formats a date as HH:MM:SS.
1693
+
1694
+ **Type Signature:**
1695
+
1696
+ ```typescript
1697
+ function formatTimeISO(date: Date): string;
1698
+ ```
1699
+
1700
+ **Parameters:**
1701
+
1702
+ - `date` - The date to format
1703
+
1704
+ **Returns:**
1705
+
1706
+ - Time string in HH:MM:SS format
1707
+
1708
+ **Examples:**
1709
+
1710
+ ```typescript
1711
+ import { formatTimeISO, now } from "@reasonabletech/utils";
1712
+
1713
+ formatTimeISO(now());
1714
+ // Result: "14:30:45"
1715
+
1716
+ // Logging with timestamp
1717
+ console.log(`[${formatTimeISO(now())}] Event occurred`);
1718
+
1719
+ // Display time only
1720
+ const displayTime = formatTimeISO(appointment.scheduledAt);
1721
+ ```
1722
+
1723
+ ---
1724
+
1725
+ ## Result Type
1726
+
1727
+ A simplified Result type inspired by Rust's Result for consistent error handling.
1728
+
1729
+ ### Types
1730
+
1731
+ ```typescript
1732
+ // Success variant
1733
+ interface Success<T> {
1734
+ success: true;
1735
+ value: T;
1736
+ error?: undefined;
1737
+ }
1738
+
1739
+ // Failure variant
1740
+ interface Failure<E> {
1741
+ success: false;
1742
+ error: E;
1743
+ value?: undefined;
1744
+ }
1745
+
1746
+ // Union type
1747
+ type Result<T, E = Error> = Success<T> | Failure<E>;
1748
+ ```
1749
+
1750
+ ### `ok(value)`
1751
+
1752
+ Creates a successful Result.
1753
+
1754
+ **Type Signature:**
1755
+
1756
+ ```typescript
1757
+ function ok<T, E = Error>(value: T): Result<T, E>;
1758
+ function ok<E = Error>(): Result<void, E>;
1759
+ ```
1760
+
1761
+ **Parameters:**
1762
+
1763
+ - `value` - Optional value to wrap in a successful Result
1764
+
1765
+ **Returns:**
1766
+
1767
+ - A successful Result containing the value
1768
+
1769
+ **Examples:**
1770
+
1771
+ ```typescript
1772
+ import { ok, Result } from "@reasonabletech/utils";
1773
+
1774
+ // With a value
1775
+ const result: Result<number> = ok(42);
1776
+ // { success: true, value: 42 }
1777
+
1778
+ // Without a value (void)
1779
+ const voidResult: Result<void> = ok();
1780
+ // { success: true, value: undefined }
1781
+
1782
+ // In a function
1783
+ function divide(a: number, b: number): Result<number> {
1784
+ if (b === 0) return err(new Error("Division by zero"));
1785
+ return ok(a / b);
1786
+ }
1787
+ ```
1788
+
1789
+ ---
1790
+
1791
+ ### `err(error)`
1792
+
1793
+ Creates an error Result.
1794
+
1795
+ **Type Signature:**
1796
+
1797
+ ```typescript
1798
+ function err<T = never, E = Error>(error: E): Result<T, E>;
1799
+ ```
1800
+
1801
+ **Parameters:**
1802
+
1803
+ - `error` - The error to wrap in an error Result
1804
+
1805
+ **Returns:**
1806
+
1807
+ - An error Result containing the error
1808
+
1809
+ **Examples:**
1810
+
1811
+ ```typescript
1812
+ import { err, Result } from "@reasonabletech/utils";
1813
+
1814
+ const result: Result<string> = err(new Error("Something went wrong"));
1815
+ // { success: false, error: Error("Something went wrong") }
1816
+
1817
+ // Custom error types
1818
+ type ValidationError = { field: string; message: string };
1819
+ const validationResult: Result<User, ValidationError> = err({
1820
+ field: "email",
1821
+ message: "Invalid email format",
1822
+ });
1823
+ ```
1824
+
1825
+ ---
1826
+
1827
+ ### `isSuccess(result)`
1828
+
1829
+ Type guard to check if a Result is successful.
1830
+
1831
+ **Type Signature:**
1832
+
1833
+ ```typescript
1834
+ function isSuccess<T, E = Error>(result: Result<T, E>): result is Success<T>;
1835
+ ```
1836
+
1837
+ **Parameters:**
1838
+
1839
+ - `result` - The Result to check
1840
+
1841
+ **Returns:**
1842
+
1843
+ - `true` if the Result is successful
1844
+
1845
+ **Examples:**
1846
+
1847
+ ```typescript
1848
+ import { isSuccess, ok, err } from "@reasonabletech/utils";
1849
+
1850
+ const success = ok(42);
1851
+ const failure = err(new Error("failed"));
1852
+
1853
+ isSuccess(success); // true
1854
+ isSuccess(failure); // false
1855
+
1856
+ // Type narrowing
1857
+ if (isSuccess(result)) {
1858
+ console.log(result.value); // TypeScript knows value exists
1859
+ }
1860
+ ```
1861
+
1862
+ ---
1863
+
1864
+ ### `isFailure(result)`
1865
+
1866
+ Type guard to check if a Result is an error.
1867
+
1868
+ **Type Signature:**
1869
+
1870
+ ```typescript
1871
+ function isFailure<T, E = Error>(result: Result<T, E>): result is Failure<E>;
1872
+ ```
1873
+
1874
+ **Parameters:**
1875
+
1876
+ - `result` - The Result to check
1877
+
1878
+ **Returns:**
1879
+
1880
+ - `true` if the Result is an error
1881
+
1882
+ **Examples:**
1883
+
1884
+ ```typescript
1885
+ import { isFailure, ok, err } from "@reasonabletech/utils";
1886
+
1887
+ if (isFailure(result)) {
1888
+ logger.error("Operation failed", result.error);
1889
+ return;
1890
+ }
1891
+ // TypeScript knows result is Success here
1892
+ ```
1893
+
1894
+ ---
1895
+
1896
+ ### `fromPromise(promise)`
1897
+
1898
+ Wraps a Promise to return a Result.
1899
+
1900
+ **Type Signature:**
1901
+
1902
+ ```typescript
1903
+ function fromPromise<T>(promise: Promise<T>): Promise<Result<T, Error>>;
1904
+ ```
1905
+
1906
+ **Parameters:**
1907
+
1908
+ - `promise` - The Promise to wrap
1909
+
1910
+ **Returns:**
1911
+
1912
+ - A Promise that resolves to a Result
1913
+
1914
+ **Examples:**
1915
+
1916
+ ```typescript
1917
+ import { fromPromise, isSuccess } from "@reasonabletech/utils";
1918
+
1919
+ // Convert throwing async function to Result
1920
+ const result = await fromPromise(fetchUser(id));
1921
+ if (isSuccess(result)) {
1922
+ return result.value;
1923
+ } else {
1924
+ logger.error("Failed to fetch user", result.error);
1925
+ return null;
1926
+ }
1927
+
1928
+ // Chain multiple operations
1929
+ const userResult = await fromPromise(fetchUser(id));
1930
+ if (!isSuccess(userResult)) return userResult;
1931
+
1932
+ const ordersResult = await fromPromise(fetchOrders(userResult.value.id));
1933
+ ```
1934
+
1935
+ ---
1936
+
1937
+ ### `map(result, fn)`
1938
+
1939
+ Maps a successful Result to a new Result with a transformed value.
1940
+
1941
+ **Type Signature:**
1942
+
1943
+ ```typescript
1944
+ function map<T, U, E = Error>(
1945
+ result: Result<T, E>,
1946
+ fn: (value: T) => U,
1947
+ ): Result<U, E>;
1948
+ ```
1949
+
1950
+ **Parameters:**
1951
+
1952
+ - `result` - The Result to map
1953
+ - `fn` - The function to apply to the value
1954
+
1955
+ **Returns:**
1956
+
1957
+ - A new Result with the transformed value or the original error
1958
+
1959
+ **Examples:**
1960
+
1961
+ ```typescript
1962
+ import { map, ok, err } from "@reasonabletech/utils";
1963
+
1964
+ const result = ok(5);
1965
+ const doubled = map(result, (x) => x * 2);
1966
+ // { success: true, value: 10 }
1967
+
1968
+ const failed = err<number>(new Error("failed"));
1969
+ const stillFailed = map(failed, (x) => x * 2);
1970
+ // { success: false, error: Error("failed") }
1971
+
1972
+ // Transform user data
1973
+ const userResult = await fetchUser(id);
1974
+ const nameResult = map(userResult, (user) => user.name);
1975
+ ```
1976
+
1977
+ ---
1978
+
1979
+ ### `mapErr(result, fn)`
1980
+
1981
+ Maps an error Result to a new Result with a transformed error.
1982
+
1983
+ **Type Signature:**
1984
+
1985
+ ```typescript
1986
+ function mapErr<T, E = Error, F = Error>(
1987
+ result: Result<T, E>,
1988
+ fn: (error: E) => F,
1989
+ ): Result<T, F>;
1990
+ ```
1991
+
1992
+ **Parameters:**
1993
+
1994
+ - `result` - The Result to map
1995
+ - `fn` - The function to apply to the error
1996
+
1997
+ **Returns:**
1998
+
1999
+ - A new Result with the transformed error or the original value
2000
+
2001
+ **Examples:**
2002
+
2003
+ ```typescript
2004
+ import { mapErr, err, ok } from "@reasonabletech/utils";
2005
+
2006
+ // Transform error type
2007
+ const result = err(new Error("DB error"));
2008
+ const apiError = mapErr(result, (e) => ({
2009
+ code: "DATABASE_ERROR",
2010
+ message: e.message,
2011
+ }));
2012
+
2013
+ // Add context to errors
2014
+ const enrichedError = mapErr(result, (e) =>
2015
+ new Error(`User fetch failed: ${e.message}`)
2016
+ );
2017
+ ```
2018
+
2019
+ ---
2020
+
2021
+ ### `andThen(result, fn)`
2022
+
2023
+ Chains a function that returns a Result after a successful Result.
2024
+
2025
+ **Type Signature:**
2026
+
2027
+ ```typescript
2028
+ function andThen<T, U, E = Error>(
2029
+ result: Result<T, E>,
2030
+ fn: (value: T) => Result<U, E>,
2031
+ ): Result<U, E>;
2032
+ ```
2033
+
2034
+ **Parameters:**
2035
+
2036
+ - `result` - The Result to chain
2037
+ - `fn` - The function to apply to the value that returns a Result
2038
+
2039
+ **Returns:**
2040
+
2041
+ - The Result returned by the function or the original error
2042
+
2043
+ **Examples:**
2044
+
2045
+ ```typescript
2046
+ import { andThen, ok, err } from "@reasonabletech/utils";
2047
+
2048
+ function parseNumber(s: string): Result<number> {
2049
+ const n = parseInt(s, 10);
2050
+ return isNaN(n) ? err(new Error("Invalid number")) : ok(n);
2051
+ }
2052
+
2053
+ function validatePositive(n: number): Result<number> {
2054
+ return n > 0 ? ok(n) : err(new Error("Must be positive"));
2055
+ }
2056
+
2057
+ // Chain validations
2058
+ const result = andThen(parseNumber("42"), validatePositive);
2059
+ // { success: true, value: 42 }
2060
+
2061
+ const failed = andThen(parseNumber("-5"), validatePositive);
2062
+ // { success: false, error: Error("Must be positive") }
2063
+ ```
2064
+
2065
+ ---
2066
+
2067
+ ### `orElse(result, fn)`
2068
+
2069
+ Applies a fallback function to an error Result.
2070
+
2071
+ **Type Signature:**
2072
+
2073
+ ```typescript
2074
+ function orElse<T, E = Error, F = Error>(
2075
+ result: Result<T, E>,
2076
+ fn: (error: E) => Result<T, F>,
2077
+ ): Result<T, F>;
2078
+ ```
2079
+
2080
+ **Parameters:**
2081
+
2082
+ - `result` - The Result to check
2083
+ - `fn` - The function to apply to the error that returns a Result
2084
+
2085
+ **Returns:**
2086
+
2087
+ - The original Result if successful, or the Result returned by the function
2088
+
2089
+ **Examples:**
2090
+
2091
+ ```typescript
2092
+ import { orElse, ok, err } from "@reasonabletech/utils";
2093
+
2094
+ // Provide fallback value
2095
+ const result = err<number>(new Error("failed"));
2096
+ const recovered = orElse(result, () => ok(0));
2097
+ // { success: true, value: 0 }
2098
+
2099
+ // Try alternative source
2100
+ const fromCache = orElse(fetchFromPrimary(), () => fetchFromCache());
2101
+ ```
2102
+
2103
+ ---
2104
+
2105
+ ### `unwrap(result)`
2106
+
2107
+ Unwraps a Result, returning the value or throwing the error.
2108
+
2109
+ **Type Signature:**
2110
+
2111
+ ```typescript
2112
+ function unwrap<T, E = Error>(result: Result<T, E>): T;
2113
+ ```
2114
+
2115
+ **Parameters:**
2116
+
2117
+ - `result` - The Result to unwrap
2118
+
2119
+ **Returns:**
2120
+
2121
+ - The value if the Result is successful
2122
+
2123
+ **Throws:**
2124
+
2125
+ - The error if the Result is an error
2126
+
2127
+ **Examples:**
2128
+
2129
+ ```typescript
2130
+ import { unwrap, ok, err } from "@reasonabletech/utils";
2131
+
2132
+ const value = unwrap(ok(42)); // 42
2133
+ unwrap(err(new Error("failed"))); // throws Error("failed")
2134
+
2135
+ // Use when you know it's safe
2136
+ const config = unwrap(loadConfig()); // throws if config fails to load
2137
+ ```
2138
+
2139
+ ---
2140
+
2141
+ ### `unwrapOr(result, defaultValue)`
2142
+
2143
+ Unwraps a Result, returning the value or a default value.
2144
+
2145
+ **Type Signature:**
2146
+
2147
+ ```typescript
2148
+ function unwrapOr<T, E = Error>(result: Result<T, E>, defaultValue: T): T;
2149
+ ```
2150
+
2151
+ **Parameters:**
2152
+
2153
+ - `result` - The Result to unwrap
2154
+ - `defaultValue` - The default value to return if the Result is an error
2155
+
2156
+ **Returns:**
2157
+
2158
+ - The value if the Result is successful, or the default value
2159
+
2160
+ **Examples:**
2161
+
2162
+ ```typescript
2163
+ import { unwrapOr, ok, err } from "@reasonabletech/utils";
2164
+
2165
+ unwrapOr(ok(42), 0); // 42
2166
+ unwrapOr(err(new Error()), 0); // 0
2167
+
2168
+ // Provide defaults
2169
+ const port = unwrapOr(parsePort(envVar), 3000);
2170
+ const timeout = unwrapOr(getConfigValue("timeout"), 5000);
2171
+ ```
2172
+
2173
+ ---
2174
+
2175
+ ### `unwrapOrElse(result, fn)`
2176
+
2177
+ Unwraps a Result, returning the value or computing a default from the error.
2178
+
2179
+ **Type Signature:**
2180
+
2181
+ ```typescript
2182
+ function unwrapOrElse<T, E = Error>(
2183
+ result: Result<T, E>,
2184
+ fn: (error: E) => T,
2185
+ ): T;
2186
+ ```
2187
+
2188
+ **Parameters:**
2189
+
2190
+ - `result` - The Result to unwrap
2191
+ - `fn` - The function to compute the default value from the error
2192
+
2193
+ **Returns:**
2194
+
2195
+ - The value if the Result is successful, or the computed default value
2196
+
2197
+ **Examples:**
2198
+
2199
+ ```typescript
2200
+ import { unwrapOrElse, err } from "@reasonabletech/utils";
2201
+
2202
+ const result = err<number>(new Error("missing"));
2203
+ const value = unwrapOrElse(result, (e) => {
2204
+ logger.warn("Using fallback", e);
2205
+ return 0;
2206
+ });
2207
+
2208
+ // Error-based fallback logic
2209
+ const data = unwrapOrElse(fetchResult, (error) => {
2210
+ if (error.code === "NOT_FOUND") return defaultData;
2211
+ throw error; // Re-throw unexpected errors
2212
+ });
2213
+ ```
2214
+
2215
+ ---
2216
+
2217
+ ### `combine(results)`
2218
+
2219
+ Combines an array of Results into a single Result containing an array of values.
2220
+
2221
+ **Type Signature:**
2222
+
2223
+ ```typescript
2224
+ function combine<T, E = Error>(results: Result<T, E>[]): Result<T[], E>;
2225
+ ```
2226
+
2227
+ **Parameters:**
2228
+
2229
+ - `results` - Array of Results to combine
2230
+
2231
+ **Returns:**
2232
+
2233
+ - A Result containing an array of values or the first error
2234
+
2235
+ **Examples:**
2236
+
2237
+ ```typescript
2238
+ import { combine, ok, err, isSuccess } from "@reasonabletech/utils";
2239
+
2240
+ // All successful
2241
+ const results = [ok(1), ok(2), ok(3)];
2242
+ const combined = combine(results);
2243
+ // { success: true, value: [1, 2, 3] }
2244
+
2245
+ // One failure (returns first error)
2246
+ const mixed = [ok(1), err(new Error("failed")), ok(3)];
2247
+ const combinedMixed = combine(mixed);
2248
+ // { success: false, error: Error("failed") }
2249
+
2250
+ // Validate multiple fields
2251
+ const validations = [
2252
+ validateEmail(email),
2253
+ validatePassword(password),
2254
+ validateUsername(username),
2255
+ ];
2256
+ const allValid = combine(validations);
2257
+ if (!isSuccess(allValid)) {
2258
+ return { error: allValid.error };
2259
+ }
2260
+ ```
2261
+
2262
+ ---
2263
+
2264
+ ## Retry Functions
2265
+
2266
+ Utilities for retrying async operations with exponential backoff and jitter.
2267
+
2268
+ ### Configuration Types
2269
+
2270
+ ```typescript
2271
+ interface RetryOptions {
2272
+ /** Maximum number of attempts (default: 3) */
2273
+ maxAttempts?: number;
2274
+ /** Initial delay in milliseconds (default: 1000) */
2275
+ initialDelay?: number;
2276
+ /** Maximum delay in milliseconds (default: 30000) */
2277
+ maxDelay?: number;
2278
+ /** Multiplier for exponential backoff (default: 2) */
2279
+ backoffMultiplier?: number;
2280
+ /** Jitter factor 0-1 (default: 0.1) */
2281
+ jitterFactor?: number;
2282
+ /** Function to determine if error should trigger retry */
2283
+ shouldRetry?: (error: unknown, attempt: number) => boolean;
2284
+ /** Callback when an attempt fails */
2285
+ onError?: (error: unknown, attempt: number) => void | Promise<void>;
2286
+ /** Custom delay calculator */
2287
+ getDelay?: (attempt: number, error: unknown) => number;
2288
+ }
2289
+
2290
+ interface RetryResult<T> {
2291
+ success: boolean;
2292
+ value?: T;
2293
+ error?: unknown;
2294
+ attempts: number;
2295
+ }
2296
+ ```
2297
+
2298
+ ### `retry(operation, options)`
2299
+
2300
+ Retry an async operation with exponential backoff and jitter.
2301
+
2302
+ **Type Signature:**
2303
+
2304
+ ```typescript
2305
+ function retry<T>(
2306
+ operation: () => Promise<T>,
2307
+ options?: RetryOptions,
2308
+ ): Promise<RetryResult<T>>;
2309
+ ```
2310
+
2311
+ **Parameters:**
2312
+
2313
+ - `operation` - The async operation to retry
2314
+ - `options` - Retry configuration options
2315
+
2316
+ **Returns:**
2317
+
2318
+ - Promise resolving to retry result with success status, value/error, and attempt count
2319
+
2320
+ **Examples:**
2321
+
2322
+ ```typescript
2323
+ import { retry } from "@reasonabletech/utils";
2324
+
2325
+ // Basic retry with defaults (3 attempts, 1s initial delay)
2326
+ const result = await retry(() => fetchData());
2327
+ if (result.success) {
2328
+ console.log(result.value);
2329
+ } else {
2330
+ console.error(`Failed after ${result.attempts} attempts`, result.error);
2331
+ }
2332
+
2333
+ // Custom configuration with error callback
2334
+ const result = await retry(() => apiClient.post("/users", userData), {
2335
+ maxAttempts: 5,
2336
+ initialDelay: 500,
2337
+ onError: (error, attempt) => {
2338
+ logger.warn("API", `Attempt ${attempt} failed`, { error });
2339
+ },
2340
+ });
2341
+
2342
+ // Use server-provided Retry-After hint
2343
+ const result = await retry(() => rateLimitedApi.call(), {
2344
+ getDelay: (attempt, error) => {
2345
+ if (error instanceof ApiError && error.retryAfter) {
2346
+ return error.retryAfter; // Use server-provided delay
2347
+ }
2348
+ return 1000 * Math.pow(2, attempt - 1); // Fallback
2349
+ },
2350
+ });
2351
+
2352
+ // Only retry specific errors
2353
+ const result = await retry(() => dbOperation(), {
2354
+ shouldRetry: (error) => {
2355
+ return error instanceof TransientError;
2356
+ },
2357
+ });
2358
+ ```
2359
+
2360
+ ---
2361
+
2362
+ ### `retryWithBackoff(operation, maxRetries, baseDelay)`
2363
+
2364
+ Simplified retry with exponential backoff that throws on failure.
2365
+
2366
+ **Type Signature:**
2367
+
2368
+ ```typescript
2369
+ function retryWithBackoff<T>(
2370
+ operation: () => Promise<T>,
2371
+ maxRetries?: number,
2372
+ baseDelay?: number,
2373
+ ): Promise<T>;
2374
+ ```
2375
+
2376
+ **Parameters:**
2377
+
2378
+ - `operation` - The async operation to retry
2379
+ - `maxRetries` - Maximum number of retries after first attempt (default: 3)
2380
+ - `baseDelay` - Base delay in milliseconds (default: 1000)
2381
+
2382
+ **Returns:**
2383
+
2384
+ - Promise resolving to the operation's result
2385
+
2386
+ **Throws:**
2387
+
2388
+ - The last error if all attempts fail
2389
+
2390
+ **Examples:**
2391
+
2392
+ ```typescript
2393
+ import { retryWithBackoff } from "@reasonabletech/utils";
2394
+
2395
+ // Retry up to 3 times with exponential backoff
2396
+ const result = await retryWithBackoff(() => fetchData(), 3, 500);
2397
+
2398
+ // Use default retries (3) and delay (1000ms)
2399
+ try {
2400
+ const user = await retryWithBackoff(() => createUser(userData));
2401
+ } catch (error) {
2402
+ // All retries exhausted
2403
+ logger.error("Failed to create user", error);
2404
+ }
2405
+ ```
2406
+
2407
+ ---
2408
+
2409
+ ### `retryWithPolling(operation, maxAttempts, interval, shouldRetry)`
2410
+
2411
+ Retry an operation with fixed interval polling (no exponential backoff).
2412
+
2413
+ **Type Signature:**
2414
+
2415
+ ```typescript
2416
+ function retryWithPolling<T>(
2417
+ operation: () => Promise<T>,
2418
+ maxAttempts: number,
2419
+ interval: number,
2420
+ shouldRetry?: (error: unknown, attempt: number) => boolean,
2421
+ ): Promise<RetryResult<T>>;
2422
+ ```
2423
+
2424
+ **Parameters:**
2425
+
2426
+ - `operation` - The async operation to retry
2427
+ - `maxAttempts` - Maximum number of attempts
2428
+ - `interval` - Fixed interval between attempts in milliseconds
2429
+ - `shouldRetry` - Optional function to determine if retry should continue
2430
+
2431
+ **Returns:**
2432
+
2433
+ - Promise resolving to retry result
2434
+
2435
+ **Examples:**
2436
+
2437
+ ```typescript
2438
+ import { retryWithPolling } from "@reasonabletech/utils";
2439
+
2440
+ // Poll for job completion every 2 seconds, up to 30 times
2441
+ const result = await retryWithPolling(
2442
+ () => checkJobStatus(jobId),
2443
+ 30,
2444
+ 2000,
2445
+ (error) => error instanceof JobPendingError,
2446
+ );
2447
+
2448
+ // Wait for resource to become available
2449
+ const resource = await retryWithPolling(
2450
+ () => fetchResource(resourceId),
2451
+ 10,
2452
+ 1000,
2453
+ );
2454
+ ```
2455
+
2456
+ ---
2457
+
2458
+ ### `sleep(ms)`
2459
+
2460
+ Sleep for the specified number of milliseconds.
2461
+
2462
+ **Type Signature:**
2463
+
2464
+ ```typescript
2465
+ function sleep(ms: number): Promise<void>;
2466
+ ```
2467
+
2468
+ **Parameters:**
2469
+
2470
+ - `ms` - Milliseconds to sleep
2471
+
2472
+ **Returns:**
2473
+
2474
+ - Promise that resolves after the delay
2475
+
2476
+ **Examples:**
2477
+
2478
+ ```typescript
2479
+ import { sleep } from "@reasonabletech/utils";
2480
+
2481
+ // Wait 1 second
2482
+ await sleep(1000);
2483
+
2484
+ // Rate limiting
2485
+ for (const item of items) {
2486
+ await processItem(item);
2487
+ await sleep(100); // 100ms between items
2488
+ }
2489
+ ```