@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.
- package/README.md +102 -0
- package/dist/datetime.js +81 -0
- package/dist/datetime.js.map +1 -0
- package/dist/index.js +347 -0
- package/dist/index.js.map +1 -0
- package/dist/object.js +48 -0
- package/dist/object.js.map +1 -0
- package/dist/result.js +80 -0
- package/dist/result.js.map +1 -0
- package/dist/retry.js +76 -0
- package/dist/retry.js.map +1 -0
- package/dist/src/async.d.ts +43 -0
- package/dist/src/async.d.ts.map +1 -0
- package/dist/src/datetime.d.ts +166 -0
- package/dist/src/datetime.d.ts.map +1 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/object.d.ts +292 -0
- package/dist/src/object.d.ts.map +1 -0
- package/dist/src/result.d.ts +113 -0
- package/dist/src/result.d.ts.map +1 -0
- package/dist/src/retry.d.ts +145 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/string.d.ts +75 -0
- package/dist/src/string.d.ts.map +1 -0
- package/dist/src/type-guards.d.ts +26 -0
- package/dist/src/type-guards.d.ts.map +1 -0
- package/dist/string.js +48 -0
- package/dist/string.js.map +1 -0
- package/docs/README.md +35 -0
- package/docs/guides/migration.md +47 -0
- package/docs/guides/usage-guide.md +99 -0
- package/docs/utility-functions.md +2489 -0
- package/package.json +100 -0
|
@@ -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
|
+
```
|