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 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.context.field}`);
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
- // Minimal by default - only name and message
62
- const { ValidationError } = createTaggedError("ValidationError");
63
- ValidationError({ message: "Email is required" });
61
+ // Static error no fields needed
62
+ const { ValidationError } = createTaggedError("ValidationError")
63
+ .withMessage(() => "Email is required");
64
+ ValidationError();
64
65
 
65
- // Chain to add context and cause when needed
66
+ // Structured error fields are spread flat on the error object
66
67
  const { ApiError } = createTaggedError("ApiError")
67
- .withContext<{ endpoint: string }>()
68
- .withCause<NetworkError | undefined>();
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, type AnyTaggedError } from "wellcrafted/error";
112
+ import { createTaggedError } from "wellcrafted/error";
112
113
 
113
114
  // Define your error with factory function
114
115
  const { ApiError, ApiErr } = createTaggedError("ApiError")
115
- .withContext<{ endpoint: string }>()
116
- .withCause<AnyTaggedError | undefined>();
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 context and cause
216
+ // Define errors with fields and message template
220
217
  const { ParseError, ParseErr } = createTaggedError("ParseError")
221
- .withContext<{ input: string }>();
218
+ .withFields<{ input: string }>()
219
+ .withMessage(({ input }) => `Invalid JSON: ${input.slice(0, 50)}`);
222
220
 
223
221
  const { NetworkError, NetworkErr } = createTaggedError("NetworkError")
224
- .withContext<{ url: string }>();
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
- .withContext<{ currentState?: string; permissions?: string }>();
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({ message: "Invalid JSON" })
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({ message: "Parse failed" });
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 context
427
+ // 1. Define service-specific errors with typed fields and message
439
428
  const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
440
- .withContext<{ isRecording: boolean }>();
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
- .withContext<{ fields: Record<string, string[]> }>();
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
- - **Default**: minimal errors with only `name` and `message`
636
- - Chain `.withContext<T>()` to add typed context
637
- - Chain `.withCause<T>()` to add typed cause
638
- - Include `| undefined` in type to make property optional but typed
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
@@ -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 for cause parameters.
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 TContext - Additional context data for the error (default: undefined = no context)
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, TContext extends JsonObject | undefined = undefined, TCause extends AnyTaggedError | undefined = undefined> = Readonly<{
31
+ type TaggedError<TName extends string = string, TFields extends JsonObject = Record<never, never>> = Readonly<{
54
32
  name: TName;
55
33
  message: string;
56
- } & WithContext<TContext> & WithCause<TCause>>;
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
- * Replaces the "Error" suffix with "Err" suffix in error type names.
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 ReplaceErrorWithErr<T extends `${string}Error`> = T extends `${infer TBase}Error` ? `${TBase}Err` : never;
51
+ type NoReservedKeys = {
52
+ name?: never;
53
+ };
70
54
  /**
71
- * Input provided to the message template function.
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 MessageInput<TName extends string, TContext extends JsonObject | undefined, TCause extends AnyTaggedError | undefined> = {
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<TName extends string, TContext extends JsonObject | undefined, TCause extends AnyTaggedError | undefined> = (input: MessageInput<TName, TContext, TCause>) => string;
85
- /**
86
- * Helper type that determines optionality based on whether T includes undefined.
87
- */
88
- type OptionalIfUndefined<T, TKey extends string> = undefined extends T ? { [K in TKey]?: Exclude<T, undefined> } : { [K in TKey]: T };
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
- * Input type for error factory functions.
91
- * `message` is optional (computed from template when omitted).
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 ErrorCallInput<TContext extends JsonObject | undefined, TCause extends AnyTaggedError | undefined> = {
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
- * The final factories object returned by `.withMessage()`.
99
- * Has factory functions, NO chain methods.
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 FinalFactories<TName extends `${string}Error`, TContext extends JsonObject | undefined, TCause extends AnyTaggedError | undefined> = { [K in TName]: (input: ErrorCallInput<TContext, TCause>) => TaggedError<TName, TContext, TCause> } & { [K in ReplaceErrorWithErr<TName>]: (input: ErrorCallInput<TContext, TCause>) => Err<TaggedError<TName, TContext, TCause>> };
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 interface for the fluent createTaggedError API.
104
- * Has chain methods only, NO factory functions.
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`, TContext extends JsonObject | undefined = undefined, TCause extends AnyTaggedError | undefined = undefined> = {
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
- * Constrains the context type for this error.
110
- * `.withContext<T>()` where T doesn't include undefined context is **required**
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
- withContext<T extends JsonObject | undefined = JsonObject | undefined>(): ErrorBuilder<TName, T, TCause>;
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
- * The builder provides `.withContext<T>()`, `.withCause<T>()`, and `.withMessage(fn)`.
132
- * `.withMessage(fn)` is **required** and **terminal** it returns the factory functions.
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 Simple error
106
+ * @example No fields, with sealed message
135
107
  * ```ts
136
- * const { RecorderBusyError, RecorderBusyErr } = createTaggedError('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 Error with context
113
+ * @example With fields, message required
141
114
  * ```ts
142
- * const { DbNotFoundError, DbNotFoundErr } = createTaggedError('DbNotFoundError')
143
- * .withContext<{ table: string; id: string }>()
144
- * .withMessage(({ context }) => `${context.table} '${context.id}' not found`);
115
+ * const { FsReadError } = createTaggedError('FsReadError')
116
+ * .withFields<{ path: string }>();
117
+ * FsReadError({ message: 'Failed to read', path: '/etc/config' });
145
118
  * ```
146
119
  *
147
- * @example Error with context and cause
120
+ * @example With fields + sealed message (template computes from fields)
148
121
  * ```ts
149
- * const { ServiceError, ServiceErr } = createTaggedError('ServiceError')
150
- * .withContext<{ operation: string }>()
151
- * .withCause<DbServiceError>()
152
- * .withMessage(({ context, cause }) =>
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;AAQK,KARO,cAAA,GAQI;EAAA,IAAA,EAAA,MAAA;EAAA,OAAc,EAAA,MAAA;CAAQ;;;;;AAIf;AAAA;KAJlB,WAYS,CAAA,QAAA,CAAA,GAAA,CAZgB,QAYhB,CAAA,SAAA,CAAA,SAAA,CAAA,GAXX,MAWW,CAAA,KAAA,EAAA,KAAA,CAAA,GAAA,CAAA,SAAA,CAAA,SAAA,CAVU,QAUV,CAAA,GAAA;EAAA,OAAY,CAAA,EATV,OASU,CATF,QASE,EAAA,SAAA,CAAA;CAAM,GAAA;EACvB,OACe,EAVT,QAUS;CAAM;;;AAEX;AAUnB;;;KAdK,SAiBW,CAAA,MAAA,CAAA,GAAA,CAjBU,MAiBV,CAAA,SAAA,CAAA,SAAA,CAAA,GAhBb,MAgBa,CAAA,KAAA,EAAA,KAAA,CAAA,GAAA,CAAA,SAAA,CAAA,SAAA,CAfQ,MAeR,CAAA,GAAA;EAAc,KAGtB,CAAA,EAjBM,OAiBN,CAjBc,MAiBd,EAAA,SAAA,CAAA;CAAK,GAAA;EAEY,KAApB,EAlBQ,MAkBR;CAAW;;;AAJJ;;;;AC7CZ;AAkDC;AAKuB,KDdZ,WCcY,CAAA,cAAA,MAAA,GAAA,MAAA,EAAA,iBDZN,UCYM,GAAA,SAAA,GAAA,SAAA,EAAA,eDXR,cCWQ,GAAA,SAAA,GAAA,SAAA,CAAA,GDVpB,QCUoB,CAAA;EAAA,IACvB,EDTO,KCSP;EAAC,OAAoC,EAAA,MAAA;AAAK,CAAA,GDPtC,WCOsC,CDP1B,QCO0B,CAAA,GDNzC,SCMyC,CDN/B,MCM+B,CAAA,CAAA;;;;ADjE3C;;;;AAM6B;AAKjB,iBCFI,mBAAA,CDEM,KAAA,EAAA,OAAA,CAAA,EAAA,MAAA;;;;AAAS,KCqD1B,mBDrD0B,CAAA,UAAA,GAAA,MAAA,OAAA,CAAA,GCsD9B,CDtD8B,SAAA,GAAA,KAAA,MAAA,OAAA,GAAA,GCsDO,KDtDP,KAAA,GAAA,KAAA;AAK/B;AAA+D;;;KC2D1D,YDlDF,CAAA,cAAA,MAAA,EAAA,iBCoDe,UDpDf,GAAA,SAAA,EAAA,eCqDa,cDrDb,GAAA,SAAA,CAAA,GAAA;EAAM,IACe,ECqDZ,KDrDY;CAAQ,GAAA,CAAA,CCqDR,QDpDA,CAAA,SAAA,CAAA,SAAA,CAAA,GCqDrB,MDrDqB,CAAA,KAAA,EAAA,KAAA,CAAA,GAAA;EAAQ,OAAhB,ECsDF,ODtDE,CCsDM,QDtDN,EAAA,SAAA,CAAA;CAAO,CAAA,GAAA,CAAA,CCuDpB,MDtDY,CAAA,SAAA,CAAA,SAAA,CAAA,GCuDX,MDvDW,CAAA,KAAA,EAAA,KAAA,CAAA,GAAA;EAAQ,KAAA,ECwDV,ODxDU,CCwDF,MDxDE,EAAA,SAAA,CAAA;AAAA,CAAA,CAAA;;;;KC6DlB,SDnDmB,CAAA,cAAA,MAAA,EAAA,iBCqDN,UDrDM,GAAA,SAAA,EAAA,eCsDR,cDtDQ,GAAA,SAAA,CAAA,GAAA,CAAA,KAAA,ECuDZ,YDvDY,CCuDC,KDvDD,ECuDQ,QDvDR,ECuDkB,MDvDlB,CAAA,EAAA,GAAA,MAAA;;;;AAEL,KC8Dd,mBD9Dc,CAAA,CAAA,EAAA,aAAA,MAAA,CAAA,GAAA,SAAA,SC8DkD,CD9DlD,GAAA,QC+DR,IDrDC,ICqDO,ODrDI,CCqDI,CDrDJ,EAAA,SAAA,CAAA,EAAA,GAAA,QCsDZ,IDpDO,GCoDA,CDpDA,EAAU;;;;;;KC2DvB,cDzDD,CAAA,iBC0Dc,UD1Dd,GAAA,SAAA,EAAA,eC2DY,cD3DZ,GAAA,SAAA,CAAA,GAAA;EAAQ,OAAA,CAAA,EAAA,MAAA;KC4DgB,6BACzB,uBACA,oBAAoB,yBACrB,2BACE,uBACA,oBAAoB;;;AA9GxB;AAkDC;KAsEI,cAjEmB,CAAA,cAAA,GAAA,MAAA,OAAA,EAAA,iBAmEN,UAnEM,GAAA,SAAA,EAAA,eAoER,cApEQ,GAAA,SAAA,CAAA,GAAA,QAsEjB,KArEN,GAAA,CAAA,KAAA,EAsEQ,cAtER,CAsEuB,QAtEvB,EAsEiC,MAtEjC,CAAA,EAAA,GAuEK,WAvEL,CAuEiB,KAvEjB,EAuEwB,QAvExB,EAuEkC,MAvElC,CAAA,EAAC,GAAA,QAyEK,mBAzEoC,CAyEhB,KAzEgB,CAAA,GAAA,CAAA,KAAA,EA0ElC,cA1EkC,CA0EnB,QA1EmB,EA0ET,MA1ES,CAAA,EAAA,GA2ErC,GA3EqC,CA2EjC,WA3EiC,CA2ErB,KA3EqB,EA2Ed,QA3Ec,EA2EJ,MA3EI,CAAA,CAAA,EAAA;;;;;;KAmFtC,YApEF,CAAA,cAAA,GAAA,MAAA,OAAA,EAAA,iBAsEe,UAtEf,GAAA,SAAA,GAAA,SAAA,EAAA,eAuEa,cAvEb,GAAA,SAAA,GAAA,SAAA,CAAA,GAAA;EAAM;;;;;EAIkB,WAAd,CAAA,UA2ED,UA3EC,GAAA,SAAA,GA2EwB,UA3ExB,GAAA,SAAA,CAAA,EAAA,EA4EP,YA5EO,CA4EM,KA5EN,EA4Ea,CA5Eb,EA4EgB,MA5EhB,CAAA;EAAO;AAAA;;;;EAQU,SACL,CAAA,UA2Eb,cA3Ea,GAAA,SAAA,GA2EgB,cA3EhB,GAAA,SAAA,CAAA,EAAA,EA4EnB,YA5EmB,CA4EN,KA5EM,EA4EC,QA5ED,EA4EW,CA5EX,CAAA;EAAK;;;AAAN;AAAA;;EASA,WAA6C,CAAA,EAAA,EA4E/D,SA5E+D,CA4ErD,KA5EqD,EA4E9C,QA5E8C,EA4EpC,MA5EoC,CAAA,CAAA,EA6EjE,cA7EiE,CA6ElD,KA7EkD,EA6E3C,QA7E2C,EA6EjC,MA7EiC,CAAA;CAAC;;;;;AAEnD;AAAA;;;;;;;;;;;;AAeI;AAAA;;;;;;;;;;;AAiBjB,iBA+EU,iBA/EV,CAAA,cAAA,GAAA,MAAA,OAAA,CAAA,CAAA,IAAA,EAgFC,KAhFD,CAAA,EAiFH,YAjFG,CAiFU,KAjFV,CAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/utils.ts"],"sourcesContent":[],"mappings":"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"}
@@ -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
- * The builder provides `.withContext<T>()`, `.withCause<T>()`, and `.withMessage(fn)`.
41
- * `.withMessage(fn)` is **required** and **terminal** it returns the factory functions.
40
+ * Returns an object with factory functions immediately available (message required),
41
+ * plus `.withFields<T>()` and `.withMessage(fn)` for further configuration.
42
42
  *
43
- * @example Simple error
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 { RecorderBusyError, RecorderBusyErr } = createTaggedError('RecorderBusyError')
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 Error with context
60
+ * @example With fields, message required
50
61
  * ```ts
51
- * const { DbNotFoundError, DbNotFoundErr } = createTaggedError('DbNotFoundError')
52
- * .withContext<{ table: string; id: string }>()
53
- * .withMessage(({ context }) => `${context.table} '${context.id}' not found`);
62
+ * const { FsReadError } = createTaggedError('FsReadError')
63
+ * .withFields<{ path: string }>();
64
+ * FsReadError({ message: 'Failed to read', path: '/etc/config' });
54
65
  * ```
55
66
  *
56
- * @example Error with context and cause
67
+ * @example With fields + sealed message (template computes from fields)
57
68
  * ```ts
58
- * const { ServiceError, ServiceErr } = createTaggedError('ServiceError')
59
- * .withContext<{ operation: string }>()
60
- * .withCause<DbServiceError>()
61
- * .withMessage(({ context, cause }) =>
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
- withContext() {
70
- return createBuilder();
71
- },
72
- withCause() {
88
+ [name]: errorConstructor,
89
+ [errName]: errConstructor,
90
+ withFields() {
73
91
  return createBuilder();
74
92
  },
75
93
  withMessage(fn) {
76
- const errorConstructor = (input) => {
77
- const messageInput = {
78
- name,
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: input.message ?? fn(messageInput),
85
- ..."context" in input ? { context: input.context } : {},
86
- ..."cause" in input ? { cause: input.cause } : {}
99
+ message,
100
+ ...fields
87
101
  };
88
102
  };
89
- const errName = name.replace(/Error$/, "Err");
90
- const errConstructor = (input) => Err(errorConstructor(input));
103
+ const sealedErrConstructor = (input) => Err(sealedErrorConstructor(input));
91
104
  return {
92
- [name]: errorConstructor,
93
- [errName]: errConstructor
105
+ [name]: sealedErrorConstructor,
106
+ [errName]: sealedErrConstructor
94
107
  };
95
108
  }
96
109
  };
@@ -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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wellcrafted",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Delightful TypeScript patterns for elegant, type-safe applications",
5
5
  "type": "module",
6
6
  "files": [