wellcrafted 0.31.0 → 0.32.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 +50 -62
- package/dist/error/index.d.ts +59 -88
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +45 -32
- package/dist/error/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,8 +21,8 @@ try {
|
|
|
21
21
|
const { data, error } = await saveUser(user);
|
|
22
22
|
if (error) {
|
|
23
23
|
switch (error.name) {
|
|
24
|
-
case "ValidationError":
|
|
25
|
-
showToast(`Invalid ${error.
|
|
24
|
+
case "ValidationError":
|
|
25
|
+
showToast(`Invalid ${error.field}`);
|
|
26
26
|
break;
|
|
27
27
|
case "AuthError":
|
|
28
28
|
redirectToLogin();
|
|
@@ -58,14 +58,15 @@ Structured, serializable errors with a fluent API
|
|
|
58
58
|
```typescript
|
|
59
59
|
import { createTaggedError } from "wellcrafted/error";
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
const { ValidationError } = createTaggedError("ValidationError")
|
|
63
|
-
|
|
61
|
+
// Static error — no fields needed
|
|
62
|
+
const { ValidationError } = createTaggedError("ValidationError")
|
|
63
|
+
.withMessage(() => "Email is required");
|
|
64
|
+
ValidationError();
|
|
64
65
|
|
|
65
|
-
//
|
|
66
|
+
// Structured error — fields are spread flat on the error object
|
|
66
67
|
const { ApiError } = createTaggedError("ApiError")
|
|
67
|
-
.
|
|
68
|
-
.
|
|
68
|
+
.withFields<{ endpoint: string }>()
|
|
69
|
+
.withMessage(({ endpoint }) => `Request to ${endpoint} failed`);
|
|
69
70
|
```
|
|
70
71
|
|
|
71
72
|
### 🔄 Query Integration
|
|
@@ -108,22 +109,18 @@ npm install wellcrafted
|
|
|
108
109
|
|
|
109
110
|
```typescript
|
|
110
111
|
import { tryAsync } from "wellcrafted/result";
|
|
111
|
-
import { createTaggedError
|
|
112
|
+
import { createTaggedError } from "wellcrafted/error";
|
|
112
113
|
|
|
113
114
|
// Define your error with factory function
|
|
114
115
|
const { ApiError, ApiErr } = createTaggedError("ApiError")
|
|
115
|
-
.
|
|
116
|
-
.
|
|
116
|
+
.withFields<{ endpoint: string }>()
|
|
117
|
+
.withMessage(({ endpoint }) => `Failed to fetch ${endpoint}`);
|
|
117
118
|
type ApiError = ReturnType<typeof ApiError>;
|
|
118
119
|
|
|
119
120
|
// Wrap any throwing operation
|
|
120
121
|
const { data, error } = await tryAsync({
|
|
121
122
|
try: () => fetch('/api/user').then(r => r.json()),
|
|
122
|
-
catch: (e) => ApiErr({
|
|
123
|
-
message: "Failed to fetch user",
|
|
124
|
-
context: { endpoint: '/api/user' },
|
|
125
|
-
cause: { name: "FetchError", message: String(e) }
|
|
126
|
-
})
|
|
123
|
+
catch: (e) => ApiErr({ endpoint: '/api/user' })
|
|
127
124
|
});
|
|
128
125
|
|
|
129
126
|
if (error) {
|
|
@@ -216,29 +213,25 @@ if (error) {
|
|
|
216
213
|
### Wrap Unsafe Operations
|
|
217
214
|
|
|
218
215
|
```typescript
|
|
219
|
-
// Define errors with
|
|
216
|
+
// Define errors with fields and message template
|
|
220
217
|
const { ParseError, ParseErr } = createTaggedError("ParseError")
|
|
221
|
-
.
|
|
218
|
+
.withFields<{ input: string }>()
|
|
219
|
+
.withMessage(({ input }) => `Invalid JSON: ${input.slice(0, 50)}`);
|
|
222
220
|
|
|
223
221
|
const { NetworkError, NetworkErr } = createTaggedError("NetworkError")
|
|
224
|
-
.
|
|
222
|
+
.withFields<{ url: string }>()
|
|
223
|
+
.withMessage(({ url }) => `Request to ${url} failed`);
|
|
225
224
|
|
|
226
225
|
// Synchronous
|
|
227
226
|
const result = trySync({
|
|
228
227
|
try: () => JSON.parse(jsonString),
|
|
229
|
-
catch: () => ParseErr({
|
|
230
|
-
message: "Invalid JSON",
|
|
231
|
-
context: { input: jsonString }
|
|
232
|
-
})
|
|
228
|
+
catch: () => ParseErr({ input: jsonString })
|
|
233
229
|
});
|
|
234
230
|
|
|
235
231
|
// Asynchronous
|
|
236
232
|
const result = await tryAsync({
|
|
237
233
|
try: () => fetch(url),
|
|
238
|
-
catch: () => NetworkErr({
|
|
239
|
-
message: "Request failed",
|
|
240
|
-
context: { url }
|
|
241
|
-
})
|
|
234
|
+
catch: () => NetworkErr({ url })
|
|
242
235
|
});
|
|
243
236
|
```
|
|
244
237
|
|
|
@@ -250,7 +243,11 @@ import { createTaggedError } from "wellcrafted/error";
|
|
|
250
243
|
import { tryAsync, Result, Ok } from "wellcrafted/result";
|
|
251
244
|
|
|
252
245
|
const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
|
|
253
|
-
.
|
|
246
|
+
.withFields<{ currentState?: string; permissions?: string }>()
|
|
247
|
+
.withMessage(({ permissions, currentState }) => {
|
|
248
|
+
if (permissions) return `Missing ${permissions} permission`;
|
|
249
|
+
return `Invalid recorder state: ${currentState}`;
|
|
250
|
+
});
|
|
254
251
|
type RecorderServiceError = ReturnType<typeof RecorderServiceError>;
|
|
255
252
|
|
|
256
253
|
export function createRecorderService() {
|
|
@@ -260,10 +257,7 @@ export function createRecorderService() {
|
|
|
260
257
|
return {
|
|
261
258
|
async startRecording(): Promise<Result<void, RecorderServiceError>> {
|
|
262
259
|
if (isRecording) {
|
|
263
|
-
return RecorderServiceErr({
|
|
264
|
-
message: "Already recording",
|
|
265
|
-
context: { currentState: 'recording' }
|
|
266
|
-
});
|
|
260
|
+
return RecorderServiceErr({ currentState: 'recording' });
|
|
267
261
|
}
|
|
268
262
|
|
|
269
263
|
return tryAsync({
|
|
@@ -273,19 +267,13 @@ export function createRecorderService() {
|
|
|
273
267
|
// ... recording setup
|
|
274
268
|
isRecording = true;
|
|
275
269
|
},
|
|
276
|
-
catch: () => RecorderServiceErr({
|
|
277
|
-
message: "Failed to start recording",
|
|
278
|
-
context: { permissions: 'microphone' }
|
|
279
|
-
})
|
|
270
|
+
catch: () => RecorderServiceErr({ permissions: 'microphone' })
|
|
280
271
|
});
|
|
281
272
|
},
|
|
282
273
|
|
|
283
274
|
async stopRecording(): Promise<Result<Blob, RecorderServiceError>> {
|
|
284
275
|
if (!isRecording) {
|
|
285
|
-
return RecorderServiceErr({
|
|
286
|
-
message: "Not currently recording",
|
|
287
|
-
context: { currentState: 'idle' }
|
|
288
|
-
});
|
|
276
|
+
return RecorderServiceErr({ currentState: 'idle' });
|
|
289
277
|
}
|
|
290
278
|
|
|
291
279
|
// Stop recording and return blob...
|
|
@@ -379,12 +367,13 @@ const { data: parsed } = trySync({
|
|
|
379
367
|
|
|
380
368
|
### Propagation Pattern (May Fail)
|
|
381
369
|
```typescript
|
|
382
|
-
const { ParseError, ParseErr } = createTaggedError("ParseError")
|
|
370
|
+
const { ParseError, ParseErr } = createTaggedError("ParseError")
|
|
371
|
+
.withMessage(() => "Invalid JSON");
|
|
383
372
|
|
|
384
373
|
// When catch can return Err<E>, function returns Result<T, E>
|
|
385
374
|
const mayFail = trySync({
|
|
386
375
|
try: () => JSON.parse(riskyJson),
|
|
387
|
-
catch: () => ParseErr(
|
|
376
|
+
catch: () => ParseErr()
|
|
388
377
|
});
|
|
389
378
|
// mayFail: Result<object, ParseError> - Must check for errors
|
|
390
379
|
if (isOk(mayFail)) {
|
|
@@ -402,7 +391,7 @@ const smartParse = trySync({
|
|
|
402
391
|
return Ok({}); // Return Ok<T> for fallback
|
|
403
392
|
}
|
|
404
393
|
// Propagate other errors
|
|
405
|
-
return ParseErr(
|
|
394
|
+
return ParseErr();
|
|
406
395
|
}
|
|
407
396
|
});
|
|
408
397
|
// smartParse: Result<object, ParseError> - Mixed handling = Result type
|
|
@@ -435,9 +424,12 @@ Based on real-world usage, here's the recommended pattern for creating services
|
|
|
435
424
|
import { createTaggedError } from "wellcrafted/error";
|
|
436
425
|
import { Result, Ok } from "wellcrafted/result";
|
|
437
426
|
|
|
438
|
-
// 1. Define service-specific errors with typed
|
|
427
|
+
// 1. Define service-specific errors with typed fields and message
|
|
439
428
|
const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
|
|
440
|
-
.
|
|
429
|
+
.withFields<{ isRecording: boolean }>()
|
|
430
|
+
.withMessage(({ isRecording }) =>
|
|
431
|
+
isRecording ? "Already recording" : "Not currently recording"
|
|
432
|
+
);
|
|
441
433
|
type RecorderServiceError = ReturnType<typeof RecorderServiceError>;
|
|
442
434
|
|
|
443
435
|
// 2. Create service with factory function
|
|
@@ -449,10 +441,7 @@ export function createRecorderService() {
|
|
|
449
441
|
return {
|
|
450
442
|
startRecording(): Result<void, RecorderServiceError> {
|
|
451
443
|
if (isRecording) {
|
|
452
|
-
return RecorderServiceErr({
|
|
453
|
-
message: "Already recording",
|
|
454
|
-
context: { isRecording }
|
|
455
|
-
});
|
|
444
|
+
return RecorderServiceErr({ isRecording });
|
|
456
445
|
}
|
|
457
446
|
|
|
458
447
|
isRecording = true;
|
|
@@ -461,10 +450,7 @@ export function createRecorderService() {
|
|
|
461
450
|
|
|
462
451
|
stopRecording(): Result<Blob, RecorderServiceError> {
|
|
463
452
|
if (!isRecording) {
|
|
464
|
-
return RecorderServiceErr({
|
|
465
|
-
message: "Not currently recording",
|
|
466
|
-
context: { isRecording }
|
|
467
|
-
});
|
|
453
|
+
return RecorderServiceErr({ isRecording });
|
|
468
454
|
}
|
|
469
455
|
|
|
470
456
|
isRecording = false;
|
|
@@ -549,7 +535,11 @@ export async function GET(request: Request) {
|
|
|
549
535
|
|
|
550
536
|
```typescript
|
|
551
537
|
const { FormError, FormErr } = createTaggedError("FormError")
|
|
552
|
-
.
|
|
538
|
+
.withFields<{ fields: Record<string, string[]> }>()
|
|
539
|
+
.withMessage(({ fields }) => {
|
|
540
|
+
const fieldNames = Object.keys(fields).join(", ");
|
|
541
|
+
return `Validation failed for: ${fieldNames}`;
|
|
542
|
+
});
|
|
553
543
|
|
|
554
544
|
function validateLoginForm(data: unknown): Result<LoginData, FormError> {
|
|
555
545
|
const errors: Record<string, string[]> = {};
|
|
@@ -559,10 +549,7 @@ function validateLoginForm(data: unknown): Result<LoginData, FormError> {
|
|
|
559
549
|
}
|
|
560
550
|
|
|
561
551
|
if (Object.keys(errors).length > 0) {
|
|
562
|
-
return FormErr({
|
|
563
|
-
message: "Validation failed",
|
|
564
|
-
context: { fields: errors }
|
|
565
|
-
});
|
|
552
|
+
return FormErr({ fields: errors });
|
|
566
553
|
}
|
|
567
554
|
|
|
568
555
|
return Ok(data as LoginData);
|
|
@@ -632,10 +619,11 @@ For comprehensive examples, service layer patterns, framework integrations, and
|
|
|
632
619
|
### Error Functions
|
|
633
620
|
- **`createTaggedError(name)`** - Creates error factory functions with fluent API
|
|
634
621
|
- Returns `{ErrorName}` (plain error) and `{ErrorName}Err` (Err-wrapped)
|
|
635
|
-
-
|
|
636
|
-
- Chain `.
|
|
637
|
-
-
|
|
638
|
-
-
|
|
622
|
+
- Chain `.withFields<T>()` to add typed fields (spread flat on the error object)
|
|
623
|
+
- Chain `.withMessage(fn)` *(optional)* to seal the message with a template — `message` is not in the factory input type when present
|
|
624
|
+
- Without `.withMessage()`, `message` is required at the call site
|
|
625
|
+
- `name` is a reserved key — prevented at compile time by `NoReservedKeys`
|
|
626
|
+
- Factory input is flat (e.g., `{ endpoint: '/api' }`), not nested
|
|
639
627
|
- **`extractErrorMessage(error)`** - Extract readable message from unknown error
|
|
640
628
|
|
|
641
629
|
### Types
|
package/dist/error/index.d.ts
CHANGED
|
@@ -14,46 +14,25 @@ type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
|
14
14
|
*/
|
|
15
15
|
type JsonObject = Record<string, JsonValue>;
|
|
16
16
|
/**
|
|
17
|
-
* Base type for any tagged error, used as a constraint
|
|
17
|
+
* Base type for any tagged error, used as a minimum constraint.
|
|
18
18
|
*/
|
|
19
19
|
type AnyTaggedError = {
|
|
20
20
|
name: string;
|
|
21
21
|
message: string;
|
|
22
22
|
};
|
|
23
|
-
/**
|
|
24
|
-
* Helper type that adds a context property.
|
|
25
|
-
* - When TContext is undefined (default): NO context property (explicit opt-in)
|
|
26
|
-
* - When TContext includes undefined (e.g., `{ foo: string } | undefined`): context is OPTIONAL but typed
|
|
27
|
-
* - When TContext is a specific type without undefined: context is REQUIRED with that exact type
|
|
28
|
-
*/
|
|
29
|
-
type WithContext<TContext> = [TContext] extends [undefined] ? Record<never, never> : [undefined] extends [TContext] ? {
|
|
30
|
-
context?: Exclude<TContext, undefined>;
|
|
31
|
-
} : {
|
|
32
|
-
context: TContext;
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Helper type that adds a cause property.
|
|
36
|
-
* - When TCause is undefined (default): NO cause property (explicit opt-in)
|
|
37
|
-
* - When TCause includes undefined (e.g., `NetworkError | undefined`): cause is OPTIONAL, constrained
|
|
38
|
-
* - When TCause is a specific type without undefined: cause is REQUIRED
|
|
39
|
-
*/
|
|
40
|
-
type WithCause<TCause> = [TCause] extends [undefined] ? Record<never, never> : [undefined] extends [TCause] ? {
|
|
41
|
-
cause?: Exclude<TCause, undefined>;
|
|
42
|
-
} : {
|
|
43
|
-
cause: TCause;
|
|
44
|
-
};
|
|
45
23
|
/**
|
|
46
24
|
* A tagged error type for type-safe error handling.
|
|
47
25
|
* Uses the `name` property as a discriminator for tagged unions.
|
|
26
|
+
* Additional fields are spread flat on the error object.
|
|
48
27
|
*
|
|
49
28
|
* @template TName - The error name (discriminator for tagged unions)
|
|
50
|
-
* @template
|
|
51
|
-
* @template TCause - The type of error that caused this error (default: undefined = no cause)
|
|
29
|
+
* @template TFields - Additional fields spread flat on the error (default: none)
|
|
52
30
|
*/
|
|
53
|
-
type TaggedError<TName extends string = string,
|
|
31
|
+
type TaggedError<TName extends string = string, TFields extends JsonObject = Record<never, never>> = Readonly<{
|
|
54
32
|
name: TName;
|
|
55
33
|
message: string;
|
|
56
|
-
} &
|
|
34
|
+
} & TFields>;
|
|
35
|
+
//# sourceMappingURL=types.d.ts.map
|
|
57
36
|
//#endregion
|
|
58
37
|
//#region src/error/utils.d.ts
|
|
59
38
|
/**
|
|
@@ -64,94 +43,86 @@ type TaggedError<TName extends string = string, TContext extends JsonObject | un
|
|
|
64
43
|
*/
|
|
65
44
|
declare function extractErrorMessage(error: unknown): string;
|
|
66
45
|
/**
|
|
67
|
-
*
|
|
46
|
+
* Constraint that prevents the reserved key `name` from appearing in fields.
|
|
47
|
+
* `message` is not reserved — when `.withMessage()` seals it, `message` is
|
|
48
|
+
* simply absent from the input type. When `.withMessage()` is not used,
|
|
49
|
+
* `message` is a built-in input handled separately from TFields.
|
|
68
50
|
*/
|
|
69
|
-
type
|
|
51
|
+
type NoReservedKeys = {
|
|
52
|
+
name?: never;
|
|
53
|
+
};
|
|
70
54
|
/**
|
|
71
|
-
*
|
|
72
|
-
* Contains everything the error will have except `message` (since that's what it computes).
|
|
55
|
+
* Replaces the "Error" suffix with "Err" suffix in error type names.
|
|
73
56
|
*/
|
|
74
|
-
type
|
|
75
|
-
name: TName;
|
|
76
|
-
} & ([TContext] extends [undefined] ? Record<never, never> : {
|
|
77
|
-
context: Exclude<TContext, undefined>;
|
|
78
|
-
}) & ([TCause] extends [undefined] ? Record<never, never> : {
|
|
79
|
-
cause: Exclude<TCause, undefined>;
|
|
80
|
-
});
|
|
57
|
+
type ReplaceErrorWithErr<T extends `${string}Error`> = T extends `${infer TBase}Error` ? `${TBase}Err` : never;
|
|
81
58
|
/**
|
|
82
59
|
* Message template function type.
|
|
60
|
+
* Receives the fields (without name/message) and returns a message string.
|
|
83
61
|
*/
|
|
84
|
-
type MessageFn<
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
62
|
+
type MessageFn<TFields extends JsonObject> = (input: TFields) => string;
|
|
63
|
+
/** Factory input when message is required (no `.withMessage()`). */
|
|
64
|
+
type RequiredMessageInput<TFields extends JsonObject> = {
|
|
65
|
+
message: string;
|
|
66
|
+
} & TFields;
|
|
89
67
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* `context` and `cause` follow the same optionality rules as before.
|
|
68
|
+
* Whether the entire input parameter can be omitted (called with no args).
|
|
69
|
+
* True when TFields is empty or all-optional (and message is sealed via withMessage).
|
|
93
70
|
*/
|
|
94
|
-
type
|
|
95
|
-
message?: string;
|
|
96
|
-
} & (TContext extends undefined ? Record<never, never> : OptionalIfUndefined<TContext, "context">) & (TCause extends undefined ? Record<never, never> : OptionalIfUndefined<TCause, "cause">);
|
|
71
|
+
type IsInputOptional<TFields extends JsonObject> = Partial<TFields> extends TFields ? true : false;
|
|
97
72
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
73
|
+
* Factories returned by `.withMessage()` — message is sealed by the template.
|
|
74
|
+
* `message` is NOT in the input type. Only has factory functions, no chain methods.
|
|
100
75
|
*/
|
|
101
|
-
type
|
|
76
|
+
type SealedFactories<TName extends `${string}Error`, TFields extends JsonObject> = { [K in TName]: IsInputOptional<TFields> extends true ? (input?: TFields) => TaggedError<TName, TFields> : (input: TFields) => TaggedError<TName, TFields> } & { [K in ReplaceErrorWithErr<TName>]: IsInputOptional<TFields> extends true ? (input?: TFields) => Err<TaggedError<TName, TFields>> : (input: TFields) => Err<TaggedError<TName, TFields>> };
|
|
102
77
|
/**
|
|
103
|
-
* Builder
|
|
104
|
-
* Has
|
|
105
|
-
* Must call `.withMessage(fn)` to get factories.
|
|
78
|
+
* Builder object returned by createTaggedError (and .withFields()).
|
|
79
|
+
* Has factory functions (message required) AND chain methods.
|
|
106
80
|
*/
|
|
107
|
-
type ErrorBuilder<TName extends `${string}Error`,
|
|
81
|
+
type ErrorBuilder<TName extends `${string}Error`, TFields extends JsonObject = Record<never, never>> = { [K in TName]: (input: RequiredMessageInput<TFields>) => TaggedError<TName, TFields> } & { [K in ReplaceErrorWithErr<TName>]: (input: RequiredMessageInput<TFields>) => Err<TaggedError<TName, TFields>> } & {
|
|
82
|
+
/** Defines additional fields spread flat on the error object. */
|
|
83
|
+
withFields<T extends JsonObject & NoReservedKeys>(): ErrorBuilder<TName, T>;
|
|
108
84
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* `.withContext<T | undefined>()` → context is **optional** but typed when provided
|
|
85
|
+
* Seals the message — the template owns it entirely.
|
|
86
|
+
* `message` is NOT in the returned factory's input type.
|
|
112
87
|
*/
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Constrains the cause type for this error.
|
|
116
|
-
* `.withCause<T>()` where T doesn't include undefined → cause is **required**
|
|
117
|
-
* `.withCause<T | undefined>()` → cause is **optional** but typed when provided
|
|
118
|
-
*/
|
|
119
|
-
withCause<T extends AnyTaggedError | undefined = AnyTaggedError | undefined>(): ErrorBuilder<TName, TContext, T>;
|
|
120
|
-
/**
|
|
121
|
-
* Terminal method that defines how the error message is computed from its data.
|
|
122
|
-
* Returns the factory functions — this is the only way to get them.
|
|
123
|
-
*
|
|
124
|
-
* @param fn - Template function that receives `{ name, context?, cause? }` and returns a message string
|
|
125
|
-
*/
|
|
126
|
-
withMessage(fn: MessageFn<TName, TContext, TCause>): FinalFactories<TName, TContext, TCause>;
|
|
88
|
+
withMessage(fn: MessageFn<TFields>): SealedFactories<TName, TFields>;
|
|
127
89
|
};
|
|
128
90
|
/**
|
|
129
91
|
* Creates a new tagged error type with a fluent builder API.
|
|
130
92
|
*
|
|
131
|
-
*
|
|
132
|
-
* `.
|
|
93
|
+
* Returns an object with factory functions immediately available (message required),
|
|
94
|
+
* plus `.withFields<T>()` and `.withMessage(fn)` for further configuration.
|
|
95
|
+
*
|
|
96
|
+
* Two mutually exclusive modes:
|
|
97
|
+
* 1. **No `.withMessage()`**: `message` is required at the call site
|
|
98
|
+
* 2. **With `.withMessage()`**: message is sealed — NOT in the input type
|
|
99
|
+
*
|
|
100
|
+
* @example No fields, message required at call site
|
|
101
|
+
* ```ts
|
|
102
|
+
* const { SimpleError, SimpleErr } = createTaggedError('SimpleError');
|
|
103
|
+
* SimpleErr({ message: 'Something went wrong' });
|
|
104
|
+
* ```
|
|
133
105
|
*
|
|
134
|
-
* @example
|
|
106
|
+
* @example No fields, with sealed message
|
|
135
107
|
* ```ts
|
|
136
|
-
* const { RecorderBusyError
|
|
108
|
+
* const { RecorderBusyError } = createTaggedError('RecorderBusyError')
|
|
137
109
|
* .withMessage(() => 'A recording is already in progress');
|
|
110
|
+
* RecorderBusyError(); // message always: 'A recording is already in progress'
|
|
138
111
|
* ```
|
|
139
112
|
*
|
|
140
|
-
* @example
|
|
113
|
+
* @example With fields, message required
|
|
141
114
|
* ```ts
|
|
142
|
-
* const {
|
|
143
|
-
* .
|
|
144
|
-
*
|
|
115
|
+
* const { FsReadError } = createTaggedError('FsReadError')
|
|
116
|
+
* .withFields<{ path: string }>();
|
|
117
|
+
* FsReadError({ message: 'Failed to read', path: '/etc/config' });
|
|
145
118
|
* ```
|
|
146
119
|
*
|
|
147
|
-
* @example
|
|
120
|
+
* @example With fields + sealed message (template computes from fields)
|
|
148
121
|
* ```ts
|
|
149
|
-
* const {
|
|
150
|
-
* .
|
|
151
|
-
* .
|
|
152
|
-
*
|
|
153
|
-
* `Operation '${context.operation}' failed: ${cause.message}`
|
|
154
|
-
* );
|
|
122
|
+
* const { ResponseError } = createTaggedError('ResponseError')
|
|
123
|
+
* .withFields<{ status: number }>()
|
|
124
|
+
* .withMessage(({ status }) => `HTTP ${status}`);
|
|
125
|
+
* ResponseError({ status: 404 }); // message: "HTTP 404"
|
|
155
126
|
* ```
|
|
156
127
|
*/
|
|
157
128
|
declare function createTaggedError<TName extends `${string}Error`>(name: TName): ErrorBuilder<TName>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/utils.ts"],"sourcesContent":[],"mappings":";;;;;;;AAIA;AAAqB,KAAT,SAAA,GAAS,MAAA,GAAA,MAAA,GAAA,OAAA,GAAA,IAAA,GAKlB,SALkB,EAAA,GAAA;EAAA,CAAA,GAKlB,EAAA,MAAA,CAAA,EACiB,SADjB;CAAS;AACiB;AAK7B;;AAAwC,KAA5B,UAAA,GAAa,MAAe,CAAA,MAAA,EAAA,SAAA,CAAA;;AAAT;AAK/B;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/utils.ts"],"sourcesContent":[],"mappings":";;;;;;;AAIA;AAAqB,KAAT,SAAA,GAAS,MAAA,GAAA,MAAA,GAAA,OAAA,GAAA,IAAA,GAKlB,SALkB,EAAA,GAAA;EAAA,CAAA,GAKlB,EAAA,MAAA,CAAA,EACiB,SADjB;CAAS;AACiB;AAK7B;;AAAwC,KAA5B,UAAA,GAAa,MAAe,CAAA,MAAA,EAAA,SAAA,CAAA;;AAAT;AAK/B;AAUY,KAVA,cAAA,GAUW;EAAA,IAAA,EAAA,MAAA;EAAA,OAEN,EAAA,MAAA;CAAU;;;;AACf;;;;ACxBZ;AA0DK,KDrCO,WCqCO,CAAA,cAAA,MAAA,GAAA,MAAA,EAAA,gBDnCF,UCmCE,GDnCW,MCmCX,CAAA,KAAA,EAAA,KAAA,CAAA,CAAA,GDlCf,QCkCe,CAAA;EAKd,IAAA,EDvCgB,KCuChB;EAAmB,OAAA,EAAA,MAAA;CAAA,GDvCwB,OCwC/C,CAAA;;;;;ADrED;;;;AAM6B;AAKjB,iBCNI,mBAAA,CDMM,KAAA,EAAA,OAAA,CAAA,EAAA,MAAA;;;;AAAS;AAK/B;AAUA;KCqCK,cAAA,GDrCkB;EAAA,IAEN,CAAA,EAAA,KAAA;CAAU;;;;AACf,KCuCP,mBDvCO,CAAA,UAAA,GAAA,MAAA,OAAA,CAAA,GCwCX,CDxCW,SAAA,GAAA,KAAA,MAAA,OAAA,GAAA,GCwC0B,KDxC1B,KAAA,GAAA,KAAA;;;;ACxBZ;AAkDC,KAoBI,SAZA,CAAA,gBAY0B,UAZZ,CAAA,GAAA,CAAA,KAAA,EAYkC,OAZlC,EAAA,GAAA,MAAA;AAAA;KAmBd,oBAdmB,CAAA,gBAckB,UAdlB,CAAA,GAAA;EAAA,OACvB,EAAA,MAAA;CAAC,GAcD,OAdqC;AAAK;AAAA;;;KAoBtC,eAdgD,CAAA,gBAchB,UAdgB,CAAA,GAepD,OAfoD,CAe5C,OAf4C,CAAA,SAe3B,OAf2B,GAAA,IAAA,GAAA,KAAA;AAAO;AAAA;;;KAyBvD,eAjBJ,CAAA,cAAA,GAAA,MAAA,OAAA,EAAA,gBAmBgB,UAnBhB,CAAA,GAAA,QAqBM,KArBC,GAqBO,eArBP,CAqBuB,OArBvB,CAAA,SAAA,IAAA,GAAA,CAAA,KAAA,CAAA,EAsBK,OAtBL,EAAA,GAsBiB,WAtBjB,CAsB6B,KAtB7B,EAsBoC,OAtBpC,CAAA,GAAA,CAAA,KAAA,EAuBI,OAvBJ,EAAA,GAuBgB,WAvBhB,CAuB4B,KAvB5B,EAuBmC,OAvBnC,CAAA,EAAA,GAMH,QAmBE,mBAnBa,CAmBO,KAnBP,CAAA,GAmBgB,eAnBhB,CAmBgC,OAnBhC,CAAA,SAAA,IAAA,GAAA,CAAA,KAAA,CAAA,EAoBP,OApBO,EAAA,GAoBK,GApBL,CAoBS,WApBT,CAoBqB,KApBrB,EAoB4B,OApB5B,CAAA,CAAA,GAAA,CAAA,KAAA,EAqBR,OArBQ,EAAA,GAqBI,GArBJ,CAqBQ,WArBR,CAqBoB,KArBpB,EAqB2B,OArB3B,CAAA,CAAA,EAAA;;;;AACa;AAAA,KA2B5B,YAjBA,CAAA,cAAe,GAAA,MAAA,OAAA,EAAA,gBAmBH,UAnBG,GAmBU,MAnBV,CAAA,KAAA,EAAA,KAAA,CAAA,CAAA,GAAA,QAqBb,KArBa,GAAA,CAAA,KAAA,EAsBX,oBAtBW,CAsBU,OAtBV,CAAA,EAAA,GAuBd,WAvBc,CAuBF,KAvBE,EAuBK,OAvBL,CAAA,EAAA,GAAA,QAyBb,mBArBA,CAqBoB,KArBpB,CAAA,GAAA,CAAA,KAAA,EAsBE,oBAtBF,CAsBuB,OAtBvB,CAAA,EAAA,GAuBD,GAvBC,CAuBG,WAvBH,CAuBe,KAvBf,EAuBsB,OAvBtB,CAAA,CAAA,EAAK,GAAA;EAA0B;EAAR,UACjB,CAAA,UAyBS,UAzBT,GAyBsB,cAzBtB,CAAA,EAAA,EAyByC,YAzBzC,CAyBsD,KAzBtD,EAyB6D,CAzB7D,CAAA;EAAO;;;;EACD,WAAiB,CAAA,EAAA,EA8BnB,SA9BmB,CA8BT,OA9BS,CAAA,CAAA,EA8BE,eA9BF,CA8BkB,KA9BlB,EA8ByB,OA9BzB,CAAA;CAAK;;;;;;;;;;;;;;;;AAId;AAAA;;;;;;;;;;;;;;;;;;;;;;AAoB2B,iBAmDtC,iBAnDsC,CAAA,cAAA,GAAA,MAAA,OAAA,CAAA,CAAA,IAAA,EAoD/C,KApD+C,CAAA,EAqDnD,YArDmD,CAqDtC,KArDsC,CAAA"}
|
package/dist/error/index.js
CHANGED
|
@@ -37,60 +37,73 @@ function extractErrorMessage(error) {
|
|
|
37
37
|
/**
|
|
38
38
|
* Creates a new tagged error type with a fluent builder API.
|
|
39
39
|
*
|
|
40
|
-
*
|
|
41
|
-
* `.
|
|
40
|
+
* Returns an object with factory functions immediately available (message required),
|
|
41
|
+
* plus `.withFields<T>()` and `.withMessage(fn)` for further configuration.
|
|
42
42
|
*
|
|
43
|
-
*
|
|
43
|
+
* Two mutually exclusive modes:
|
|
44
|
+
* 1. **No `.withMessage()`**: `message` is required at the call site
|
|
45
|
+
* 2. **With `.withMessage()`**: message is sealed — NOT in the input type
|
|
46
|
+
*
|
|
47
|
+
* @example No fields, message required at call site
|
|
44
48
|
* ```ts
|
|
45
|
-
* const {
|
|
49
|
+
* const { SimpleError, SimpleErr } = createTaggedError('SimpleError');
|
|
50
|
+
* SimpleErr({ message: 'Something went wrong' });
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example No fields, with sealed message
|
|
54
|
+
* ```ts
|
|
55
|
+
* const { RecorderBusyError } = createTaggedError('RecorderBusyError')
|
|
46
56
|
* .withMessage(() => 'A recording is already in progress');
|
|
57
|
+
* RecorderBusyError(); // message always: 'A recording is already in progress'
|
|
47
58
|
* ```
|
|
48
59
|
*
|
|
49
|
-
* @example
|
|
60
|
+
* @example With fields, message required
|
|
50
61
|
* ```ts
|
|
51
|
-
* const {
|
|
52
|
-
* .
|
|
53
|
-
*
|
|
62
|
+
* const { FsReadError } = createTaggedError('FsReadError')
|
|
63
|
+
* .withFields<{ path: string }>();
|
|
64
|
+
* FsReadError({ message: 'Failed to read', path: '/etc/config' });
|
|
54
65
|
* ```
|
|
55
66
|
*
|
|
56
|
-
* @example
|
|
67
|
+
* @example With fields + sealed message (template computes from fields)
|
|
57
68
|
* ```ts
|
|
58
|
-
* const {
|
|
59
|
-
* .
|
|
60
|
-
* .
|
|
61
|
-
*
|
|
62
|
-
* `Operation '${context.operation}' failed: ${cause.message}`
|
|
63
|
-
* );
|
|
69
|
+
* const { ResponseError } = createTaggedError('ResponseError')
|
|
70
|
+
* .withFields<{ status: number }>()
|
|
71
|
+
* .withMessage(({ status }) => `HTTP ${status}`);
|
|
72
|
+
* ResponseError({ status: 404 }); // message: "HTTP 404"
|
|
64
73
|
* ```
|
|
65
74
|
*/
|
|
66
75
|
function createTaggedError(name) {
|
|
76
|
+
const errName = name.replace(/Error$/, "Err");
|
|
67
77
|
const createBuilder = () => {
|
|
78
|
+
const errorConstructor = (input) => {
|
|
79
|
+
const { message,...fields } = input;
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
message,
|
|
83
|
+
...fields
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const errConstructor = (input) => Err(errorConstructor(input));
|
|
68
87
|
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
withCause() {
|
|
88
|
+
[name]: errorConstructor,
|
|
89
|
+
[errName]: errConstructor,
|
|
90
|
+
withFields() {
|
|
73
91
|
return createBuilder();
|
|
74
92
|
},
|
|
75
93
|
withMessage(fn) {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
..."context" in input ? { context: input.context } : {},
|
|
80
|
-
..."cause" in input ? { cause: input.cause } : {}
|
|
81
|
-
};
|
|
94
|
+
const sealedErrorConstructor = (input) => {
|
|
95
|
+
const fields = input ?? {};
|
|
96
|
+
const message = fn(fields);
|
|
82
97
|
return {
|
|
83
98
|
name,
|
|
84
|
-
message
|
|
85
|
-
...
|
|
86
|
-
..."cause" in input ? { cause: input.cause } : {}
|
|
99
|
+
message,
|
|
100
|
+
...fields
|
|
87
101
|
};
|
|
88
102
|
};
|
|
89
|
-
const
|
|
90
|
-
const errConstructor = (input) => Err(errorConstructor(input));
|
|
103
|
+
const sealedErrConstructor = (input) => Err(sealedErrorConstructor(input));
|
|
91
104
|
return {
|
|
92
|
-
[name]:
|
|
93
|
-
[errName]:
|
|
105
|
+
[name]: sealedErrorConstructor,
|
|
106
|
+
[errName]: sealedErrConstructor
|
|
94
107
|
};
|
|
95
108
|
}
|
|
96
109
|
};
|
package/dist/error/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["error: unknown","name: TName","fn: MessageFn<TName, TContext, TCause>","input: ErrorCallInput<TContext, TCause>"],"sources":["../../src/error/utils.ts"],"sourcesContent":["import type {\n\tTaggedError,\n\tAnyTaggedError,\n\tJsonObject,\n} from \"./types.js\";\nimport { Err } from \"../result/result.js\";\n\n/**\n * Extracts a readable error message from an unknown error value\n *\n * @param error - The unknown error to extract a message from\n * @returns A string representation of the error\n */\nexport function extractErrorMessage(error: unknown): string {\n\t// Handle Error instances\n\tif (error instanceof Error) {\n\t\treturn error.message;\n\t}\n\n\t// Handle primitives\n\tif (typeof error === \"string\") return error;\n\tif (\n\t\ttypeof error === \"number\" ||\n\t\ttypeof error === \"boolean\" ||\n\t\ttypeof error === \"bigint\"\n\t)\n\t\treturn String(error);\n\tif (typeof error === \"symbol\") return error.toString();\n\tif (error === null) return \"null\";\n\tif (error === undefined) return \"undefined\";\n\n\t// Handle arrays\n\tif (Array.isArray(error)) return JSON.stringify(error);\n\n\t// Handle plain objects\n\tif (typeof error === \"object\") {\n\t\tconst errorObj = error as Record<string, unknown>;\n\n\t\t// Check common error properties\n\t\tconst messageProps = [\n\t\t\t\"message\",\n\t\t\t\"error\",\n\t\t\t\"description\",\n\t\t\t\"title\",\n\t\t\t\"reason\",\n\t\t\t\"details\",\n\t\t] as const;\n\t\tfor (const prop of messageProps) {\n\t\t\tif (prop in errorObj && typeof errorObj[prop] === \"string\") {\n\t\t\t\treturn errorObj[prop];\n\t\t\t}\n\t\t}\n\n\t\t// Fallback to JSON stringification\n\t\ttry {\n\t\t\treturn JSON.stringify(error);\n\t\t} catch {\n\t\t\treturn String(error);\n\t\t}\n\t}\n\n\t// Final fallback\n\treturn String(error);\n}\n\n/**\n * Replaces the \"Error\" suffix with \"Err\" suffix in error type names.\n */\ntype ReplaceErrorWithErr<T extends `${string}Error`> =\n\tT extends `${infer TBase}Error` ? `${TBase}Err` : never;\n\n// =============================================================================\n// Message Function Types\n// =============================================================================\n\n/**\n * Input provided to the message template function.\n * Contains everything the error will have except `message` (since that's what it computes).\n */\ntype MessageInput<\n\tTName extends string,\n\tTContext extends JsonObject | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = { name: TName } & ([TContext] extends [undefined]\n\t? Record<never, never>\n\t: { context: Exclude<TContext, undefined> }) &\n\t([TCause] extends [undefined]\n\t\t? Record<never, never>\n\t\t: { cause: Exclude<TCause, undefined> });\n\n/**\n * Message template function type.\n */\ntype MessageFn<\n\tTName extends string,\n\tTContext extends JsonObject | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = (input: MessageInput<TName, TContext, TCause>) => string;\n\n// =============================================================================\n// Factory Input Types\n// =============================================================================\n\n/**\n * Helper type that determines optionality based on whether T includes undefined.\n */\ntype OptionalIfUndefined<T, TKey extends string> = undefined extends T\n\t? { [K in TKey]?: Exclude<T, undefined> }\n\t: { [K in TKey]: T };\n\n/**\n * Input type for error factory functions.\n * `message` is optional (computed from template when omitted).\n * `context` and `cause` follow the same optionality rules as before.\n */\ntype ErrorCallInput<\n\tTContext extends JsonObject | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = { message?: string } & (TContext extends undefined\n\t? Record<never, never>\n\t: OptionalIfUndefined<TContext, \"context\">) &\n\t(TCause extends undefined\n\t\t? Record<never, never>\n\t\t: OptionalIfUndefined<TCause, \"cause\">);\n\n// =============================================================================\n// Builder & Factory Types\n// =============================================================================\n\n/**\n * The final factories object returned by `.withMessage()`.\n * Has factory functions, NO chain methods.\n */\ntype FinalFactories<\n\tTName extends `${string}Error`,\n\tTContext extends JsonObject | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = {\n\t[K in TName]: (\n\t\tinput: ErrorCallInput<TContext, TCause>,\n\t) => TaggedError<TName, TContext, TCause>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: (\n\t\tinput: ErrorCallInput<TContext, TCause>,\n\t) => Err<TaggedError<TName, TContext, TCause>>;\n};\n\n/**\n * Builder interface for the fluent createTaggedError API.\n * Has chain methods only, NO factory functions.\n * Must call `.withMessage(fn)` to get factories.\n */\ntype ErrorBuilder<\n\tTName extends `${string}Error`,\n\tTContext extends JsonObject | undefined = undefined,\n\tTCause extends AnyTaggedError | undefined = undefined,\n> = {\n\t/**\n\t * Constrains the context type for this error.\n\t * `.withContext<T>()` where T doesn't include undefined → context is **required**\n\t * `.withContext<T | undefined>()` → context is **optional** but typed when provided\n\t */\n\twithContext<\n\t\tT extends JsonObject | undefined = JsonObject | undefined,\n\t>(): ErrorBuilder<TName, T, TCause>;\n\n\t/**\n\t * Constrains the cause type for this error.\n\t * `.withCause<T>()` where T doesn't include undefined → cause is **required**\n\t * `.withCause<T | undefined>()` → cause is **optional** but typed when provided\n\t */\n\twithCause<\n\t\tT extends AnyTaggedError | undefined = AnyTaggedError | undefined,\n\t>(): ErrorBuilder<TName, TContext, T>;\n\n\t/**\n\t * Terminal method that defines how the error message is computed from its data.\n\t * Returns the factory functions — this is the only way to get them.\n\t *\n\t * @param fn - Template function that receives `{ name, context?, cause? }` and returns a message string\n\t */\n\twithMessage(\n\t\tfn: MessageFn<TName, TContext, TCause>,\n\t): FinalFactories<TName, TContext, TCause>;\n};\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Creates a new tagged error type with a fluent builder API.\n *\n * The builder provides `.withContext<T>()`, `.withCause<T>()`, and `.withMessage(fn)`.\n * `.withMessage(fn)` is **required** and **terminal** — it returns the factory functions.\n *\n * @example Simple error\n * ```ts\n * const { RecorderBusyError, RecorderBusyErr } = createTaggedError('RecorderBusyError')\n * .withMessage(() => 'A recording is already in progress');\n * ```\n *\n * @example Error with context\n * ```ts\n * const { DbNotFoundError, DbNotFoundErr } = createTaggedError('DbNotFoundError')\n * .withContext<{ table: string; id: string }>()\n * .withMessage(({ context }) => `${context.table} '${context.id}' not found`);\n * ```\n *\n * @example Error with context and cause\n * ```ts\n * const { ServiceError, ServiceErr } = createTaggedError('ServiceError')\n * .withContext<{ operation: string }>()\n * .withCause<DbServiceError>()\n * .withMessage(({ context, cause }) =>\n * `Operation '${context.operation}' failed: ${cause.message}`\n * );\n * ```\n */\nexport function createTaggedError<TName extends `${string}Error`>(\n\tname: TName,\n): ErrorBuilder<TName> {\n\tconst createBuilder = <\n\t\tTContext extends JsonObject | undefined = undefined,\n\t\tTCause extends AnyTaggedError | undefined = undefined,\n\t>(): ErrorBuilder<TName, TContext, TCause> => {\n\t\treturn {\n\t\t\twithContext<\n\t\t\t\tT extends JsonObject | undefined = JsonObject | undefined,\n\t\t\t>() {\n\t\t\t\treturn createBuilder<T, TCause>();\n\t\t\t},\n\t\t\twithCause<\n\t\t\t\tT extends AnyTaggedError | undefined = AnyTaggedError | undefined,\n\t\t\t>() {\n\t\t\t\treturn createBuilder<TContext, T>();\n\t\t\t},\n\t\t\twithMessage(\n\t\t\t\tfn: MessageFn<TName, TContext, TCause>,\n\t\t\t): FinalFactories<TName, TContext, TCause> {\n\t\t\t\tconst errorConstructor = (\n\t\t\t\t\tinput: ErrorCallInput<TContext, TCause>,\n\t\t\t\t) => {\n\t\t\t\t\tconst messageInput = {\n\t\t\t\t\t\tname,\n\t\t\t\t\t\t...(\"context\" in input ? { context: input.context } : {}),\n\t\t\t\t\t\t...(\"cause\" in input ? { cause: input.cause } : {}),\n\t\t\t\t\t} as MessageInput<TName, TContext, TCause>;\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\tinput.message ?? fn(messageInput),\n\t\t\t\t\t\t...(\"context\" in input ? { context: input.context } : {}),\n\t\t\t\t\t\t...(\"cause\" in input ? { cause: input.cause } : {}),\n\t\t\t\t\t} as unknown as TaggedError<TName, TContext, TCause>;\n\t\t\t\t};\n\n\t\t\t\tconst errName = name.replace(\n\t\t\t\t\t/Error$/,\n\t\t\t\t\t\"Err\",\n\t\t\t\t) as ReplaceErrorWithErr<TName>;\n\t\t\t\tconst errConstructor = (\n\t\t\t\t\tinput: ErrorCallInput<TContext, TCause>,\n\t\t\t\t) => Err(errorConstructor(input));\n\n\t\t\t\treturn {\n\t\t\t\t\t[name]: errorConstructor,\n\t\t\t\t\t[errName]: errConstructor,\n\t\t\t\t} as FinalFactories<TName, TContext, TCause>;\n\t\t\t},\n\t\t} as ErrorBuilder<TName, TContext, TCause>;\n\t};\n\n\treturn createBuilder();\n}\n"],"mappings":";;;;;;;;;AAaA,SAAgB,oBAAoBA,OAAwB;AAE3D,KAAI,iBAAiB,MACpB,QAAO,MAAM;AAId,YAAW,UAAU,SAAU,QAAO;AACtC,YACQ,UAAU,mBACV,UAAU,oBACV,UAAU,SAEjB,QAAO,OAAO,MAAM;AACrB,YAAW,UAAU,SAAU,QAAO,MAAM,UAAU;AACtD,KAAI,UAAU,KAAM,QAAO;AAC3B,KAAI,iBAAqB,QAAO;AAGhC,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,KAAK,UAAU,MAAM;AAGtD,YAAW,UAAU,UAAU;EAC9B,MAAM,WAAW;EAGjB,MAAM,eAAe;GACpB;GACA;GACA;GACA;GACA;GACA;EACA;AACD,OAAK,MAAM,QAAQ,aAClB,KAAI,QAAQ,mBAAmB,SAAS,UAAU,SACjD,QAAO,SAAS;AAKlB,MAAI;AACH,UAAO,KAAK,UAAU,MAAM;EAC5B,QAAO;AACP,UAAO,OAAO,MAAM;EACpB;CACD;AAGD,QAAO,OAAO,MAAM;AACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4JD,SAAgB,kBACfC,MACsB;CACtB,MAAM,gBAAgB,MAGwB;AAC7C,SAAO;GACN,cAEI;AACH,WAAO,eAA0B;GACjC;GACD,YAEI;AACH,WAAO,eAA4B;GACnC;GACD,YACCC,IAC0C;IAC1C,MAAM,mBAAmB,CACxBC,UACI;KACJ,MAAM,eAAe;MACpB;MACA,GAAI,aAAa,QAAQ,EAAE,SAAS,MAAM,QAAS,IAAG,CAAE;MACxD,GAAI,WAAW,QAAQ,EAAE,OAAO,MAAM,MAAO,IAAG,CAAE;KAClD;AAED,YAAO;MACN;MACA,SACC,MAAM,WAAW,GAAG,aAAa;MAClC,GAAI,aAAa,QAAQ,EAAE,SAAS,MAAM,QAAS,IAAG,CAAE;MACxD,GAAI,WAAW,QAAQ,EAAE,OAAO,MAAM,MAAO,IAAG,CAAE;KAClD;IACD;IAED,MAAM,UAAU,KAAK,QACpB,UACA,MACA;IACD,MAAM,iBAAiB,CACtBA,UACI,IAAI,iBAAiB,MAAM,CAAC;AAEjC,WAAO;MACL,OAAO;MACP,UAAU;IACX;GACD;EACD;CACD;AAED,QAAO,eAAe;AACtB"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["error: unknown","name: TName","input: { message: string } & TFields","fn: MessageFn<TFields>","input?: TFields"],"sources":["../../src/error/utils.ts"],"sourcesContent":["import type { TaggedError, JsonObject } from \"./types.js\";\nimport { Err } from \"../result/result.js\";\n\n/**\n * Extracts a readable error message from an unknown error value\n *\n * @param error - The unknown error to extract a message from\n * @returns A string representation of the error\n */\nexport function extractErrorMessage(error: unknown): string {\n\t// Handle Error instances\n\tif (error instanceof Error) {\n\t\treturn error.message;\n\t}\n\n\t// Handle primitives\n\tif (typeof error === \"string\") return error;\n\tif (\n\t\ttypeof error === \"number\" ||\n\t\ttypeof error === \"boolean\" ||\n\t\ttypeof error === \"bigint\"\n\t)\n\t\treturn String(error);\n\tif (typeof error === \"symbol\") return error.toString();\n\tif (error === null) return \"null\";\n\tif (error === undefined) return \"undefined\";\n\n\t// Handle arrays\n\tif (Array.isArray(error)) return JSON.stringify(error);\n\n\t// Handle plain objects\n\tif (typeof error === \"object\") {\n\t\tconst errorObj = error as Record<string, unknown>;\n\n\t\t// Check common error properties\n\t\tconst messageProps = [\n\t\t\t\"message\",\n\t\t\t\"error\",\n\t\t\t\"description\",\n\t\t\t\"title\",\n\t\t\t\"reason\",\n\t\t\t\"details\",\n\t\t] as const;\n\t\tfor (const prop of messageProps) {\n\t\t\tif (prop in errorObj && typeof errorObj[prop] === \"string\") {\n\t\t\t\treturn errorObj[prop];\n\t\t\t}\n\t\t}\n\n\t\t// Fallback to JSON stringification\n\t\ttry {\n\t\t\treturn JSON.stringify(error);\n\t\t} catch {\n\t\t\treturn String(error);\n\t\t}\n\t}\n\n\t// Final fallback\n\treturn String(error);\n}\n\n/**\n * Constraint that prevents the reserved key `name` from appearing in fields.\n * `message` is not reserved — when `.withMessage()` seals it, `message` is\n * simply absent from the input type. When `.withMessage()` is not used,\n * `message` is a built-in input handled separately from TFields.\n */\ntype NoReservedKeys = { name?: never };\n\n/**\n * Replaces the \"Error\" suffix with \"Err\" suffix in error type names.\n */\ntype ReplaceErrorWithErr<T extends `${string}Error`> =\n\tT extends `${infer TBase}Error` ? `${TBase}Err` : never;\n\n/**\n * Message template function type.\n * Receives the fields (without name/message) and returns a message string.\n */\ntype MessageFn<TFields extends JsonObject> = (input: TFields) => string;\n\n// =============================================================================\n// Factory Input Types\n// =============================================================================\n\n/** Factory input when message is required (no `.withMessage()`). */\ntype RequiredMessageInput<TFields extends JsonObject> = { message: string } &\n\tTFields;\n\n/**\n * Whether the entire input parameter can be omitted (called with no args).\n * True when TFields is empty or all-optional (and message is sealed via withMessage).\n */\ntype IsInputOptional<TFields extends JsonObject> =\n\tPartial<TFields> extends TFields ? true : false;\n\n// =============================================================================\n// Builder & Factory Types\n// =============================================================================\n\n/**\n * Factories returned by `.withMessage()` — message is sealed by the template.\n * `message` is NOT in the input type. Only has factory functions, no chain methods.\n */\ntype SealedFactories<\n\tTName extends `${string}Error`,\n\tTFields extends JsonObject,\n> = {\n\t[K in TName]: IsInputOptional<TFields> extends true\n\t\t? (input?: TFields) => TaggedError<TName, TFields>\n\t\t: (input: TFields) => TaggedError<TName, TFields>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: IsInputOptional<TFields> extends true\n\t\t? (input?: TFields) => Err<TaggedError<TName, TFields>>\n\t\t: (input: TFields) => Err<TaggedError<TName, TFields>>;\n};\n\n/**\n * Builder object returned by createTaggedError (and .withFields()).\n * Has factory functions (message required) AND chain methods.\n */\ntype ErrorBuilder<\n\tTName extends `${string}Error`,\n\tTFields extends JsonObject = Record<never, never>,\n> = {\n\t[K in TName]: (\n\t\tinput: RequiredMessageInput<TFields>,\n\t) => TaggedError<TName, TFields>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: (\n\t\tinput: RequiredMessageInput<TFields>,\n\t) => Err<TaggedError<TName, TFields>>;\n} & {\n\t/** Defines additional fields spread flat on the error object. */\n\twithFields<T extends JsonObject & NoReservedKeys>(): ErrorBuilder<TName, T>;\n\n\t/**\n\t * Seals the message — the template owns it entirely.\n\t * `message` is NOT in the returned factory's input type.\n\t */\n\twithMessage(fn: MessageFn<TFields>): SealedFactories<TName, TFields>;\n};\n\n// =============================================================================\n// Implementation\n// =============================================================================\n\n/**\n * Creates a new tagged error type with a fluent builder API.\n *\n * Returns an object with factory functions immediately available (message required),\n * plus `.withFields<T>()` and `.withMessage(fn)` for further configuration.\n *\n * Two mutually exclusive modes:\n * 1. **No `.withMessage()`**: `message` is required at the call site\n * 2. **With `.withMessage()`**: message is sealed — NOT in the input type\n *\n * @example No fields, message required at call site\n * ```ts\n * const { SimpleError, SimpleErr } = createTaggedError('SimpleError');\n * SimpleErr({ message: 'Something went wrong' });\n * ```\n *\n * @example No fields, with sealed message\n * ```ts\n * const { RecorderBusyError } = createTaggedError('RecorderBusyError')\n * .withMessage(() => 'A recording is already in progress');\n * RecorderBusyError(); // message always: 'A recording is already in progress'\n * ```\n *\n * @example With fields, message required\n * ```ts\n * const { FsReadError } = createTaggedError('FsReadError')\n * .withFields<{ path: string }>();\n * FsReadError({ message: 'Failed to read', path: '/etc/config' });\n * ```\n *\n * @example With fields + sealed message (template computes from fields)\n * ```ts\n * const { ResponseError } = createTaggedError('ResponseError')\n * .withFields<{ status: number }>()\n * .withMessage(({ status }) => `HTTP ${status}`);\n * ResponseError({ status: 404 }); // message: \"HTTP 404\"\n * ```\n */\nexport function createTaggedError<TName extends `${string}Error`>(\n\tname: TName,\n): ErrorBuilder<TName> {\n\tconst errName = name.replace(\n\t\t/Error$/,\n\t\t\"Err\",\n\t) as ReplaceErrorWithErr<TName>;\n\n\tconst createBuilder = <\n\t\tTFields extends JsonObject = Record<never, never>,\n\t>(): ErrorBuilder<TName, TFields> => {\n\t\tconst errorConstructor = (\n\t\t\tinput: { message: string } & TFields,\n\t\t) => {\n\t\t\tconst { message, ...fields } = input;\n\t\t\treturn {\n\t\t\t\tname,\n\t\t\t\tmessage,\n\t\t\t\t...fields,\n\t\t\t} as unknown as TaggedError<TName, TFields>;\n\t\t};\n\n\t\tconst errConstructor = (\n\t\t\tinput: { message: string } & TFields,\n\t\t) => Err(errorConstructor(input));\n\n\t\treturn {\n\t\t\t[name]: errorConstructor,\n\t\t\t[errName]: errConstructor,\n\n\t\t\twithFields<T extends JsonObject & NoReservedKeys>() {\n\t\t\t\treturn createBuilder<T>();\n\t\t\t},\n\n\t\t\twithMessage(\n\t\t\t\tfn: MessageFn<TFields>,\n\t\t\t): SealedFactories<TName, TFields> {\n\t\t\t\tconst sealedErrorConstructor = (input?: TFields) => {\n\t\t\t\t\tconst fields = (input ?? {}) as TFields;\n\t\t\t\t\tconst message = fn(fields);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t...fields,\n\t\t\t\t\t} as unknown as TaggedError<TName, TFields>;\n\t\t\t\t};\n\n\t\t\t\tconst sealedErrConstructor = (input?: TFields) =>\n\t\t\t\t\tErr(sealedErrorConstructor(input));\n\n\t\t\t\treturn {\n\t\t\t\t\t[name]: sealedErrorConstructor,\n\t\t\t\t\t[errName]: sealedErrConstructor,\n\t\t\t\t} as SealedFactories<TName, TFields>;\n\t\t\t},\n\t\t} as ErrorBuilder<TName, TFields>;\n\t};\n\n\treturn createBuilder();\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,oBAAoBA,OAAwB;AAE3D,KAAI,iBAAiB,MACpB,QAAO,MAAM;AAId,YAAW,UAAU,SAAU,QAAO;AACtC,YACQ,UAAU,mBACV,UAAU,oBACV,UAAU,SAEjB,QAAO,OAAO,MAAM;AACrB,YAAW,UAAU,SAAU,QAAO,MAAM,UAAU;AACtD,KAAI,UAAU,KAAM,QAAO;AAC3B,KAAI,iBAAqB,QAAO;AAGhC,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,KAAK,UAAU,MAAM;AAGtD,YAAW,UAAU,UAAU;EAC9B,MAAM,WAAW;EAGjB,MAAM,eAAe;GACpB;GACA;GACA;GACA;GACA;GACA;EACA;AACD,OAAK,MAAM,QAAQ,aAClB,KAAI,QAAQ,mBAAmB,SAAS,UAAU,SACjD,QAAO,SAAS;AAKlB,MAAI;AACH,UAAO,KAAK,UAAU,MAAM;EAC5B,QAAO;AACP,UAAO,OAAO,MAAM;EACpB;CACD;AAGD,QAAO,OAAO,MAAM;AACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8HD,SAAgB,kBACfC,MACsB;CACtB,MAAM,UAAU,KAAK,QACpB,UACA,MACA;CAED,MAAM,gBAAgB,MAEe;EACpC,MAAM,mBAAmB,CACxBC,UACI;GACJ,MAAM,EAAE,QAAS,GAAG,QAAQ,GAAG;AAC/B,UAAO;IACN;IACA;IACA,GAAG;GACH;EACD;EAED,MAAM,iBAAiB,CACtBA,UACI,IAAI,iBAAiB,MAAM,CAAC;AAEjC,SAAO;IACL,OAAO;IACP,UAAU;GAEX,aAAoD;AACnD,WAAO,eAAkB;GACzB;GAED,YACCC,IACkC;IAClC,MAAM,yBAAyB,CAACC,UAAoB;KACnD,MAAM,SAAU,SAAS,CAAE;KAC3B,MAAM,UAAU,GAAG,OAAO;AAC1B,YAAO;MACN;MACA;MACA,GAAG;KACH;IACD;IAED,MAAM,uBAAuB,CAACA,UAC7B,IAAI,uBAAuB,MAAM,CAAC;AAEnC,WAAO;MACL,OAAO;MACP,UAAU;IACX;GACD;EACD;CACD;AAED,QAAO,eAAe;AACtB"}
|