wellcrafted 0.26.0 → 0.28.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
@@ -54,12 +54,18 @@ function getUser(id: UserId) { /* ... */ }
54
54
  ```
55
55
 
56
56
  ### 📋 Tagged Errors
57
- Structured, serializable errors with convenient factory functions
57
+ Structured, serializable errors with a fluent API
58
58
  ```typescript
59
59
  import { createTaggedError } from "wellcrafted/error";
60
60
 
61
- const { ApiError, ApiErr } = createTaggedError("ApiError");
62
- // ApiError() creates error object, ApiErr() creates Err-wrapped error
61
+ // Minimal by default - only name and message
62
+ const { ValidationError } = createTaggedError("ValidationError");
63
+ ValidationError({ message: "Email is required" });
64
+
65
+ // Chain to add context and cause when needed
66
+ const { ApiError } = createTaggedError("ApiError")
67
+ .withContext<{ endpoint: string }>()
68
+ .withCause<NetworkError | undefined>();
63
69
  ```
64
70
 
65
71
  ### 🔄 Query Integration
@@ -100,19 +106,21 @@ npm install wellcrafted
100
106
 
101
107
  ```typescript
102
108
  import { tryAsync } from "wellcrafted/result";
103
- import { createTaggedError } from "wellcrafted/error";
109
+ import { createTaggedError, type AnyTaggedError } from "wellcrafted/error";
104
110
 
105
111
  // Define your error with factory function
106
- const { ApiError, ApiErr } = createTaggedError("ApiError");
112
+ const { ApiError, ApiErr } = createTaggedError("ApiError")
113
+ .withContext<{ endpoint: string }>()
114
+ .withCause<AnyTaggedError | undefined>();
107
115
  type ApiError = ReturnType<typeof ApiError>;
108
116
 
109
117
  // Wrap any throwing operation
110
118
  const { data, error } = await tryAsync({
111
119
  try: () => fetch('/api/user').then(r => r.json()),
112
- catch: (error) => ApiErr({
120
+ catch: (e) => ApiErr({
113
121
  message: "Failed to fetch user",
114
122
  context: { endpoint: '/api/user' },
115
- cause: error
123
+ cause: { name: "FetchError", message: String(e) }
116
124
  })
117
125
  });
118
126
 
@@ -206,25 +214,28 @@ if (error) {
206
214
  ### Wrap Unsafe Operations
207
215
 
208
216
  ```typescript
217
+ // Define errors with context and cause
218
+ const { ParseError, ParseErr } = createTaggedError("ParseError")
219
+ .withContext<{ input: string }>();
220
+
221
+ const { NetworkError, NetworkErr } = createTaggedError("NetworkError")
222
+ .withContext<{ url: string }>();
223
+
209
224
  // Synchronous
210
225
  const result = trySync({
211
226
  try: () => JSON.parse(jsonString),
212
- catch: (error) => Err({
213
- name: "ParseError",
227
+ catch: () => ParseErr({
214
228
  message: "Invalid JSON",
215
- context: { input: jsonString },
216
- cause: error
229
+ context: { input: jsonString }
217
230
  })
218
231
  });
219
232
 
220
- // Asynchronous
233
+ // Asynchronous
221
234
  const result = await tryAsync({
222
235
  try: () => fetch(url),
223
- catch: (error) => Err({
224
- name: "NetworkError",
236
+ catch: () => NetworkErr({
225
237
  message: "Request failed",
226
- context: { url },
227
- cause: error
238
+ context: { url }
228
239
  })
229
240
  });
230
241
  ```
@@ -234,9 +245,10 @@ const result = await tryAsync({
234
245
  ```typescript
235
246
  // 1. Service Layer - Pure business logic
236
247
  import { createTaggedError } from "wellcrafted/error";
237
- import { tryAsync, Result } from "wellcrafted/result";
248
+ import { tryAsync, Result, Ok } from "wellcrafted/result";
238
249
 
239
- const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError");
250
+ const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
251
+ .withContext<{ currentState?: string; permissions?: string }>();
240
252
  type RecorderServiceError = ReturnType<typeof RecorderServiceError>;
241
253
 
242
254
  export function createRecorderService() {
@@ -248,8 +260,7 @@ export function createRecorderService() {
248
260
  if (isRecording) {
249
261
  return RecorderServiceErr({
250
262
  message: "Already recording",
251
- context: { currentState: 'recording' },
252
- cause: undefined
263
+ context: { currentState: 'recording' }
253
264
  });
254
265
  }
255
266
 
@@ -260,10 +271,9 @@ export function createRecorderService() {
260
271
  // ... recording setup
261
272
  isRecording = true;
262
273
  },
263
- catch: (error) => RecorderServiceErr({
274
+ catch: () => RecorderServiceErr({
264
275
  message: "Failed to start recording",
265
- context: { permissions: 'microphone' },
266
- cause: error
276
+ context: { permissions: 'microphone' }
267
277
  })
268
278
  });
269
279
  },
@@ -272,11 +282,10 @@ export function createRecorderService() {
272
282
  if (!isRecording) {
273
283
  return RecorderServiceErr({
274
284
  message: "Not currently recording",
275
- context: { currentState: 'idle' },
276
- cause: undefined
285
+ context: { currentState: 'idle' }
277
286
  });
278
287
  }
279
-
288
+
280
289
  // Stop recording and return blob...
281
290
  isRecording = false;
282
291
  return Ok(currentBlob!);
@@ -367,10 +376,12 @@ const { data: parsed } = trySync({
367
376
 
368
377
  ### Propagation Pattern (May Fail)
369
378
  ```typescript
370
- // When catch can return Err<E>, function returns Result<T, E>
379
+ const { ParseError, ParseErr } = createTaggedError("ParseError");
380
+
381
+ // When catch can return Err<E>, function returns Result<T, E>
371
382
  const mayFail = trySync({
372
383
  try: () => JSON.parse(riskyJson),
373
- catch: (error) => ParseErr({ message: "Invalid JSON", cause: error })
384
+ catch: () => ParseErr({ message: "Invalid JSON" })
374
385
  });
375
386
  // mayFail: Result<object, ParseError> - Must check for errors
376
387
  if (isOk(mayFail)) {
@@ -382,13 +393,13 @@ if (isOk(mayFail)) {
382
393
  ```typescript
383
394
  const smartParse = trySync({
384
395
  try: () => JSON.parse(input),
385
- catch: (error) => {
396
+ catch: () => {
386
397
  // Recover from empty input
387
398
  if (input.trim() === "") {
388
399
  return Ok({}); // Return Ok<T> for fallback
389
400
  }
390
- // Propagate other errors
391
- return ParseErr({ message: "Parse failed", cause: error });
401
+ // Propagate other errors
402
+ return ParseErr({ message: "Parse failed" });
392
403
  }
393
404
  });
394
405
  // smartParse: Result<object, ParseError> - Mixed handling = Result type
@@ -419,40 +430,40 @@ Based on real-world usage, here's the recommended pattern for creating services
419
430
 
420
431
  ```typescript
421
432
  import { createTaggedError } from "wellcrafted/error";
433
+ import { Result, Ok } from "wellcrafted/result";
422
434
 
423
- // 1. Define service-specific errors
424
- const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError");
435
+ // 1. Define service-specific errors with typed context
436
+ const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError")
437
+ .withContext<{ isRecording: boolean }>();
425
438
  type RecorderServiceError = ReturnType<typeof RecorderServiceError>;
426
439
 
427
440
  // 2. Create service with factory function
428
441
  export function createRecorderService() {
429
442
  // Private state in closure
430
443
  let isRecording = false;
431
-
444
+
432
445
  // Return object with methods
433
446
  return {
434
447
  startRecording(): Result<void, RecorderServiceError> {
435
448
  if (isRecording) {
436
449
  return RecorderServiceErr({
437
450
  message: "Already recording",
438
- context: { isRecording },
439
- cause: undefined
451
+ context: { isRecording }
440
452
  });
441
453
  }
442
-
454
+
443
455
  isRecording = true;
444
456
  return Ok(undefined);
445
457
  },
446
-
458
+
447
459
  stopRecording(): Result<Blob, RecorderServiceError> {
448
460
  if (!isRecording) {
449
461
  return RecorderServiceErr({
450
- message: "Not currently recording",
451
- context: { isRecording },
452
- cause: undefined
462
+ message: "Not currently recording",
463
+ context: { isRecording }
453
464
  });
454
465
  }
455
-
466
+
456
467
  isRecording = false;
457
468
  return Ok(new Blob(["audio data"]));
458
469
  }
@@ -534,22 +545,23 @@ export async function GET(request: Request) {
534
545
  <summary><b>Form Validation</b></summary>
535
546
 
536
547
  ```typescript
548
+ const { FormError, FormErr } = createTaggedError("FormError")
549
+ .withContext<{ fields: Record<string, string[]> }>();
550
+
537
551
  function validateLoginForm(data: unknown): Result<LoginData, FormError> {
538
552
  const errors: Record<string, string[]> = {};
539
-
553
+
540
554
  if (!isValidEmail(data?.email)) {
541
555
  errors.email = ["Invalid email format"];
542
556
  }
543
-
557
+
544
558
  if (Object.keys(errors).length > 0) {
545
- return Err({
546
- name: "FormError",
559
+ return FormErr({
547
560
  message: "Validation failed",
548
- context: { fields: errors },
549
- cause: undefined
561
+ context: { fields: errors }
550
562
  });
551
563
  }
552
-
564
+
553
565
  return Ok(data as LoginData);
554
566
  }
555
567
  ```
@@ -615,10 +627,12 @@ For comprehensive examples, service layer patterns, framework integrations, and
615
627
  - **`defineMutation(options)`** - Define a mutation with dual interface (`.options()` + `.execute()`)
616
628
 
617
629
  ### Error Functions
618
- - **`createTaggedError(name)`** - Creates error factory functions
619
- - Returns two functions: `{ErrorName}` and `{ErrorName}Err`
620
- - The first creates plain error objects
621
- - The second creates Err-wrapped errors
630
+ - **`createTaggedError(name)`** - Creates error factory functions with fluent API
631
+ - Returns `{ErrorName}` (plain error) and `{ErrorName}Err` (Err-wrapped)
632
+ - **Default**: minimal errors with only `name` and `message`
633
+ - Chain `.withContext<T>()` to add typed context
634
+ - Chain `.withCause<T>()` to add typed cause
635
+ - Include `| undefined` in type to make property optional but typed
622
636
  - **`extractErrorMessage(error)`** - Extract readable message from unknown error
623
637
 
624
638
  ### Types
@@ -10,81 +10,77 @@ type AnyTaggedError = {
10
10
  };
11
11
  /**
12
12
  * Helper type that adds a context property.
13
- * - When TContext is undefined (default): context is OPTIONAL with loose typing
13
+ * - When TContext is undefined (default): NO context property (explicit opt-in)
14
14
  * - When TContext includes undefined (e.g., `{ foo: string } | undefined`): context is OPTIONAL but typed
15
15
  * - When TContext is a specific type without undefined: context is REQUIRED with that exact type
16
16
  *
17
- * This allows users to specify "optional but typed" context by passing a union with undefined.
17
+ * This follows Rust's explicit error philosophy: context must be explicitly added via .withContext<T>().
18
18
  */
19
- type WithContext<TContext> = [TContext] extends [undefined] ? {
20
- context?: Record<string, unknown>;
21
- } : [undefined] extends [TContext] ? {
19
+ type WithContext<TContext> = [TContext] extends [undefined] ? {} : [undefined] extends [TContext] ? {
22
20
  context?: Exclude<TContext, undefined>;
23
21
  } : {
24
22
  context: TContext;
25
23
  };
26
24
  /**
27
25
  * Helper type that adds a cause property.
28
- * - When TCause is undefined (default): cause is OPTIONAL, any tagged error allowed
26
+ * - When TCause is undefined (default): NO cause property (explicit opt-in)
29
27
  * - When TCause includes undefined (e.g., `NetworkError | undefined`): cause is OPTIONAL, constrained
30
- * - When TCause is a specific type: cause is OPTIONAL but constrained to that type
28
+ * - When TCause is a specific type without undefined: cause is REQUIRED
31
29
  *
32
- * Note: cause is always optional at runtime (errors can be created without causes),
33
- * but when TCause is specified, it constrains what cause types are allowed.
30
+ * This follows Rust's explicit error philosophy: cause must be explicitly added via .withCause<T>().
34
31
  * Using brackets to prevent distributive conditional behavior with union types.
35
32
  */
36
- type WithCause<TCause> = [TCause] extends [undefined] ? {
37
- cause?: AnyTaggedError;
38
- } : [undefined] extends [TCause] ? {
33
+ type WithCause<TCause> = [TCause] extends [undefined] ? {} : [undefined] extends [TCause] ? {
39
34
  cause?: Exclude<TCause, undefined>;
40
35
  } : {
41
- cause?: TCause;
36
+ cause: TCause;
42
37
  };
43
38
  /**
44
39
  * Creates a tagged error type for type-safe error handling.
45
40
  * Uses the `name` property as a discriminator for tagged unions.
46
41
  *
47
- * The `cause` property enables error chaining, creating a JSON-serializable
48
- * call stack. Each error wraps its cause, building a complete trace of how
49
- * an error propagated through your application layers.
42
+ * **Explicit Opt-In Philosophy (Rust-inspired):**
43
+ * By default, errors only have `name` and `message`. Context and cause must be
44
+ * explicitly added via type parameters. This follows Rust's thiserror pattern
45
+ * where error properties are intentional architectural decisions.
50
46
  *
51
47
  * **Type Parameter Behavior:**
52
- * - When `TContext` is `undefined` (default): `context` is OPTIONAL with type `Record<string, unknown>`
53
- * - When `TContext` is `{ ... } | undefined`: `context` is OPTIONAL but typed (use union for optional typed context)
54
- * - When `TContext` is specified without undefined: `context` is REQUIRED with that exact type
55
- * - When `TCause` is `undefined` (default): `cause` is OPTIONAL, any `AnyTaggedError` allowed
56
- * - When `TCause` is specified: `cause` is OPTIONAL but constrained to that type
48
+ * - When `TContext` is `undefined` (default): NO context property
49
+ * - When `TContext` is `{ ... } | undefined`: `context` is OPTIONAL but typed
50
+ * - When `TContext` is specified without undefined: `context` is REQUIRED
51
+ * - When `TCause` is `undefined` (default): NO cause property
52
+ * - When `TCause` is `{ ... } | undefined`: `cause` is OPTIONAL but typed
53
+ * - When `TCause` is specified without undefined: `cause` is REQUIRED
57
54
  *
58
55
  * @template TName - The error name (discriminator for tagged unions)
59
- * @template TContext - Additional context data for the error (default: undefined = optional loose context)
60
- * @template TCause - The type of error that caused this error (default: undefined = optional any cause)
56
+ * @template TContext - Additional context data for the error (default: undefined = no context)
57
+ * @template TCause - The type of error that caused this error (default: undefined = no cause)
61
58
  *
62
59
  * @example
63
60
  * ```ts
64
- * // Flexible error (context and cause optional, loosely typed)
61
+ * // Minimal error (no context, no cause)
65
62
  * type ValidationError = TaggedError<"ValidationError">;
66
63
  * const validationError: ValidationError = {
67
64
  * name: "ValidationError",
68
65
  * message: "Input is required"
69
66
  * };
70
- * // validationError.context is optional, typed as Record<string, unknown> | undefined
67
+ * // validationError only has name and message
71
68
  *
72
- * // Error with required context (fixed context mode)
69
+ * // Error with required context
73
70
  * type NetworkError = TaggedError<"NetworkError", { host: string; port: number }>;
74
71
  * const networkError: NetworkError = {
75
72
  * name: "NetworkError",
76
73
  * message: "Socket timeout",
77
74
  * context: { host: "db.example.com", port: 5432 } // Required!
78
75
  * };
79
- * const host = networkError.context.host; // Type-safe, no optional chaining needed
80
76
  *
81
77
  * // Error with OPTIONAL but TYPED context (union with undefined)
82
78
  * type LogError = TaggedError<"LogError", { file: string; line: number } | undefined>;
83
- * const logError1: LogError = { name: "LogError", message: "Parse failed" }; // OK - no context
84
- * const logError2: LogError = { name: "LogError", message: "Parse failed", context: { file: "app.ts", line: 42 } }; // OK - typed context
79
+ * const logError1: LogError = { name: "LogError", message: "Parse failed" }; // OK
80
+ * const logError2: LogError = { name: "LogError", message: "Parse failed", context: { file: "app.ts", line: 42 } }; // OK
85
81
  *
86
- * // Error with fixed context and constrained cause type
87
- * type DatabaseError = TaggedError<"DatabaseError", { operation: string }, NetworkError>;
82
+ * // Error with required context and optional cause
83
+ * type DatabaseError = TaggedError<"DatabaseError", { operation: string }, NetworkError | undefined>;
88
84
  * const dbError: DatabaseError = {
89
85
  * name: "DatabaseError",
90
86
  * message: "Failed to connect to database",
@@ -158,136 +154,6 @@ declare function extractErrorMessage(error: unknown): string;
158
154
  * ```
159
155
  */
160
156
  type ReplaceErrorWithErr<T extends `${string}Error`> = T extends `${infer TBase}Error` ? `${TBase}Err` : never;
161
- /**
162
- * Return type when neither context nor cause are constrained (flexible mode).
163
- * Context and cause are optional with loose typing.
164
- */
165
- type FlexibleFactories<TName extends `${string}Error`> = { [K in TName]: FlexibleErrorConstructor<K> } & { [K in ReplaceErrorWithErr<TName>]: FlexibleErrConstructor<TName> };
166
- /**
167
- * Return type when context is fixed.
168
- * Context is required with exact type; cause is optional.
169
- */
170
- type ContextFixedFactories<TName extends `${string}Error`, TContext extends Record<string, unknown>> = { [K in TName]: ContextFixedErrorConstructor<K, TContext> } & { [K in ReplaceErrorWithErr<TName>]: ContextFixedErrConstructor<TName, TContext> };
171
- /**
172
- * Return type when both context and cause are fixed.
173
- * Context is required; cause is optional but constrained to specific type.
174
- */
175
- type BothFixedFactories<TName extends `${string}Error`, TContext extends Record<string, unknown>, TCause extends AnyTaggedError> = { [K in TName]: BothFixedErrorConstructor<K, TContext, TCause> } & { [K in ReplaceErrorWithErr<TName>]: BothFixedErrConstructor<TName, TContext, TCause> };
176
- /**
177
- * Creates plain TaggedError objects with flexible context and cause.
178
- * Single signature: context and cause are optional with loose typing.
179
- */
180
- type FlexibleErrorConstructor<TName extends string> = (input: {
181
- message: string;
182
- context?: Record<string, unknown>;
183
- cause?: AnyTaggedError;
184
- }) => TaggedError<TName>;
185
- /**
186
- * Creates Err-wrapped TaggedError objects with flexible context and cause.
187
- * Single signature: context and cause are optional with loose typing.
188
- */
189
- type FlexibleErrConstructor<TName extends string> = (input: {
190
- message: string;
191
- context?: Record<string, unknown>;
192
- cause?: AnyTaggedError;
193
- }) => Err<TaggedError<TName>>;
194
- /**
195
- * Creates plain TaggedError objects with fixed context.
196
- * Single signature: context is required, cause is optional.
197
- */
198
- type ContextFixedErrorConstructor<TName extends string, TContext extends Record<string, unknown>> = (input: {
199
- message: string;
200
- context: TContext;
201
- cause?: AnyTaggedError;
202
- }) => TaggedError<TName, TContext>;
203
- /**
204
- * Creates Err-wrapped TaggedError objects with fixed context.
205
- * Single signature: context is required, cause is optional.
206
- */
207
- type ContextFixedErrConstructor<TName extends string, TContext extends Record<string, unknown>> = (input: {
208
- message: string;
209
- context: TContext;
210
- cause?: AnyTaggedError;
211
- }) => Err<TaggedError<TName, TContext>>;
212
- /**
213
- * Creates plain TaggedError objects with both context and cause fixed.
214
- * Single signature: context is required, cause is optional but constrained.
215
- */
216
- type BothFixedErrorConstructor<TName extends string, TContext extends Record<string, unknown>, TCause extends AnyTaggedError> = (input: {
217
- message: string;
218
- context: TContext;
219
- cause?: TCause;
220
- }) => TaggedError<TName, TContext, TCause>;
221
- /**
222
- * Creates Err-wrapped TaggedError objects with both context and cause fixed.
223
- * Single signature: context is required, cause is optional but constrained.
224
- */
225
- type BothFixedErrConstructor<TName extends string, TContext extends Record<string, unknown>, TCause extends AnyTaggedError> = (input: {
226
- message: string;
227
- context: TContext;
228
- cause?: TCause;
229
- }) => Err<TaggedError<TName, TContext, TCause>>;
230
- /**
231
- * Creates two factory functions for building tagged errors with type-safe error chaining.
232
- *
233
- * @deprecated Use `defineError()` instead for a cleaner fluent API:
234
- * ```ts
235
- * // Before
236
- * const { FileError } = createTaggedError<'FileError', { path: string }>('FileError')
237
- *
238
- * // After
239
- * const { FileError } = defineError('FileError')
240
- * .withContext<{ path: string }>()
241
- * ```
242
- *
243
- * Given an error name like "NetworkError", this returns:
244
- * - `NetworkError`: Creates a plain TaggedError object
245
- * - `NetworkErr`: Creates a TaggedError object wrapped in an Err result
246
- *
247
- * **Three usage modes:**
248
- *
249
- * 1. **Flexible mode** (no type params): Context and cause are optional, loosely typed
250
- * 2. **Fixed context mode** (TContext specified): Context is required with exact shape
251
- * 3. **Both fixed mode** (TContext + TCause): Context required, cause constrained
252
- *
253
- * **ReturnType works correctly in all modes:**
254
- * ```ts
255
- * const { NetworkError } = createTaggedError('NetworkError');
256
- * type NetworkError = ReturnType<typeof NetworkError>;
257
- * // = TaggedError<'NetworkError'> with optional context/cause
258
- * ```
259
- *
260
- * @template TName - The name of the error type (must end with "Error")
261
- * @template TContext - Optional fixed context shape (makes context required)
262
- * @template TCause - Optional fixed cause type (constrains cause type if provided)
263
- * @param name - The name of the error type (must end with "Error")
264
- *
265
- * @example
266
- * ```ts
267
- * // Mode 1: Flexible - context and cause optional, loosely typed
268
- * const { NetworkError, NetworkErr } = createTaggedError('NetworkError');
269
- * NetworkError({ message: 'Connection failed' });
270
- * NetworkError({ message: 'Timeout', context: { url: 'https://...' } });
271
- * NetworkError({ message: 'Failed', cause: otherError });
272
- *
273
- * // Type annotation works with ReturnType:
274
- * type NetworkError = ReturnType<typeof NetworkError>;
275
- *
276
- * // Mode 2: Fixed context - context REQUIRED with exact shape
277
- * type BlobContext = { filename: string; code: 'INVALID' | 'TOO_LARGE' };
278
- * const { BlobError, BlobErr } = createTaggedError<'BlobError', BlobContext>('BlobError');
279
- * BlobError({ message: 'Invalid', context: { filename: 'x', code: 'INVALID' } });
280
- * // BlobError({ message: 'Error' }); // Type error - context required
281
- *
282
- * // Mode 3: Fixed context + cause - context required, cause constrained
283
- * const { ApiError, ApiErr } = createTaggedError<'ApiError', { endpoint: string }, NetworkError>('ApiError');
284
- * ApiError({ message: 'Failed', context: { endpoint: '/users' } });
285
- * ApiError({ message: 'Failed', context: { endpoint: '/users' }, cause: networkError });
286
- * ```
287
- */
288
- declare function createTaggedError<TName extends `${string}Error`>(name: TName): FlexibleFactories<TName>;
289
- declare function createTaggedError<TName extends `${string}Error`, TContext extends Record<string, unknown>>(name: TName): ContextFixedFactories<TName, TContext>;
290
- declare function createTaggedError<TName extends `${string}Error`, TContext extends Record<string, unknown>, TCause extends AnyTaggedError>(name: TName): BothFixedFactories<TName, TContext, TCause>;
291
157
  /**
292
158
  * Helper type that determines optionality based on whether T includes undefined.
293
159
  * - If T includes undefined → property is optional
@@ -296,20 +162,21 @@ declare function createTaggedError<TName extends `${string}Error`, TContext exte
296
162
  type OptionalIfUndefined<T, TKey extends string> = undefined extends T ? { [K in TKey]?: Exclude<T, undefined> } : { [K in TKey]: T };
297
163
  /**
298
164
  * Input type for error constructors with fluent API context/cause handling.
165
+ *
166
+ * Follows explicit opt-in philosophy:
167
+ * - When TContext/TCause is undefined: property doesn't exist
168
+ * - When TContext/TCause includes undefined: property is optional but typed
169
+ * - When TContext/TCause is a specific type: property is required
299
170
  */
300
171
  type ErrorInput<TContext extends Record<string, unknown> | undefined, TCause extends AnyTaggedError | undefined> = {
301
172
  message: string;
302
- } & (TContext extends undefined ? {
303
- context?: Record<string, unknown>;
304
- } : OptionalIfUndefined<TContext, "context">) & (TCause extends undefined ? {
305
- cause?: AnyTaggedError;
306
- } : OptionalIfUndefined<TCause, "cause">);
173
+ } & (TContext extends undefined ? {} : OptionalIfUndefined<TContext, "context">) & (TCause extends undefined ? {} : OptionalIfUndefined<TCause, "cause">);
307
174
  /**
308
- * The factories object returned by defineError and its builder methods.
175
+ * The factories object returned by createTaggedError and its builder methods.
309
176
  */
310
177
  type ErrorFactories<TName extends `${string}Error`, TContext extends Record<string, unknown> | undefined, TCause extends AnyTaggedError | undefined> = { [K in TName]: (input: ErrorInput<TContext, TCause>) => TaggedError<TName, TContext, TCause> } & { [K in ReplaceErrorWithErr<TName>]: (input: ErrorInput<TContext, TCause>) => Err<TaggedError<TName, TContext, TCause>> };
311
178
  /**
312
- * Builder interface for the fluent defineError API.
179
+ * Builder interface for the fluent createTaggedError API.
313
180
  * Provides chaining methods and the error factories.
314
181
  */
315
182
  type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string, unknown> | undefined = undefined, TCause extends AnyTaggedError | undefined = undefined> = ErrorFactories<TName, TContext, TCause> & {
@@ -324,7 +191,7 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
324
191
  *
325
192
  * @example Required context
326
193
  * ```ts
327
- * const { FileError } = defineError('FileError')
194
+ * const { FileError } = createTaggedError('FileError')
328
195
  * .withContext<{ path: string }>()
329
196
  *
330
197
  * FileError({ message: 'Not found', context: { path: '/etc/config' } }) // OK
@@ -333,14 +200,23 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
333
200
  *
334
201
  * @example Optional but typed context
335
202
  * ```ts
336
- * const { LogError } = defineError('LogError')
203
+ * const { LogError } = createTaggedError('LogError')
337
204
  * .withContext<{ file: string; line: number } | undefined>()
338
205
  *
339
206
  * LogError({ message: 'Parse error' }) // OK
340
207
  * LogError({ message: 'Parse error', context: { file: 'app.ts', line: 42 } }) // OK
341
208
  * ```
209
+ *
210
+ * @example Default (no generic): permissive optional context
211
+ * ```ts
212
+ * const { FlexError } = createTaggedError('FlexError')
213
+ * .withContext() // Defaults to Record<string, unknown> | undefined
214
+ *
215
+ * FlexError({ message: 'Error' }) // OK - context is optional
216
+ * FlexError({ message: 'Error', context: { anything: 'works' } }) // OK
217
+ * ```
342
218
  */
343
- withContext<T extends Record<string, unknown> | undefined>(): ErrorBuilder<TName, T, TCause>;
219
+ withContext<T extends Record<string, unknown> | undefined = Record<string, unknown> | undefined>(): ErrorBuilder<TName, T, TCause>;
344
220
  /**
345
221
  * Constrains the cause type for this error.
346
222
  *
@@ -354,7 +230,7 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
354
230
  *
355
231
  * @example Optional typed cause (common)
356
232
  * ```ts
357
- * const { ServiceError } = defineError('ServiceError')
233
+ * const { ServiceError } = createTaggedError('ServiceError')
358
234
  * .withCause<DbError | CacheError | undefined>()
359
235
  *
360
236
  * ServiceError({ message: 'Failed' }) // OK
@@ -363,27 +239,37 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
363
239
  *
364
240
  * @example Required cause (for wrapper errors)
365
241
  * ```ts
366
- * const { UnhandledError } = defineError('UnhandledError')
242
+ * const { UnhandledError } = createTaggedError('UnhandledError')
367
243
  * .withCause<AnyTaggedError>()
368
244
  *
369
245
  * UnhandledError({ message: 'Unexpected', cause: originalError }) // OK
370
246
  * UnhandledError({ message: 'Unexpected' }) // Type error: cause required
371
247
  * ```
248
+ *
249
+ * @example Default (no generic): permissive optional cause
250
+ * ```ts
251
+ * const { FlexError } = createTaggedError('FlexError')
252
+ * .withCause() // Defaults to AnyTaggedError | undefined
253
+ *
254
+ * FlexError({ message: 'Error' }) // OK - cause is optional
255
+ * FlexError({ message: 'Error', cause: anyTaggedError }) // OK
256
+ * ```
372
257
  */
373
- withCause<T extends AnyTaggedError | undefined>(): ErrorBuilder<TName, TContext, T>;
258
+ withCause<T extends AnyTaggedError | undefined = AnyTaggedError | undefined>(): ErrorBuilder<TName, TContext, T>;
374
259
  };
375
260
  /**
376
- * Defines a new tagged error type with a fluent builder API.
261
+ * Creates a new tagged error type with a fluent builder API.
377
262
  *
378
263
  * Returns an object containing:
379
264
  * - `{Name}Error`: Factory function that creates plain TaggedError objects
380
265
  * - `{Name}Err`: Factory function that creates Err-wrapped TaggedError objects
381
- * - `withContext<T>()`: Chain method to constrain context type
382
- * - `withCause<T>()`: Chain method to constrain cause type
266
+ * - `withContext<T>()`: Chain method to add context type
267
+ * - `withCause<T>()`: Chain method to add cause type
383
268
  *
384
- * **Default behavior (no chaining):**
385
- * - `context` is optional and accepts any `Record<string, unknown>`
386
- * - `cause` is optional and accepts any `AnyTaggedError`
269
+ * **Explicit Opt-In (Rust-inspired):**
270
+ * By default, errors only have `{ name, message }`. Context and cause must be
271
+ * explicitly added via `.withContext<T>()` and `.withCause<T>()`. This follows
272
+ * Rust's thiserror pattern where error properties are intentional decisions.
387
273
  *
388
274
  * **Optionality via type unions:**
389
275
  * Both `withContext` and `withCause` determine optionality based on whether
@@ -394,17 +280,17 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
394
280
  * @template TName - The name of the error type (must end with "Error")
395
281
  * @param name - The name of the error type
396
282
  *
397
- * @example Simple error (flexible mode)
283
+ * @example Minimal error (no context, no cause)
398
284
  * ```ts
399
- * const { NetworkError, NetworkErr } = defineError('NetworkError')
285
+ * const { NetworkError, NetworkErr } = createTaggedError('NetworkError')
400
286
  *
401
287
  * NetworkError({ message: 'Connection failed' })
402
- * NetworkError({ message: 'Timeout', context: { url: 'https://...' } })
288
+ * // Error only has { name: 'NetworkError', message: 'Connection failed' }
403
289
  * ```
404
290
  *
405
291
  * @example Required context
406
292
  * ```ts
407
- * const { ApiError, ApiErr } = defineError('ApiError')
293
+ * const { ApiError, ApiErr } = createTaggedError('ApiError')
408
294
  * .withContext<{ endpoint: string; status: number }>()
409
295
  *
410
296
  * ApiError({ message: 'Failed', context: { endpoint: '/users', status: 500 } })
@@ -413,7 +299,7 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
413
299
  *
414
300
  * @example Optional typed cause
415
301
  * ```ts
416
- * const { ServiceError } = defineError('ServiceError')
302
+ * const { ServiceError } = createTaggedError('ServiceError')
417
303
  * .withCause<DbError | CacheError | undefined>()
418
304
  *
419
305
  * ServiceError({ message: 'Failed' }) // OK
@@ -422,15 +308,22 @@ type ErrorBuilder<TName extends `${string}Error`, TContext extends Record<string
422
308
  *
423
309
  * @example Full example with both
424
310
  * ```ts
425
- * const { UserServiceError } = defineError('UserServiceError')
311
+ * const { UserServiceError } = createTaggedError('UserServiceError')
426
312
  * .withContext<{ userId: string }>()
427
313
  * .withCause<RepoError | undefined>()
428
314
  *
429
315
  * // Type extraction works
430
316
  * type UserServiceError = ReturnType<typeof UserServiceError>
431
317
  * ```
318
+ *
319
+ * @example Permissive mode (if you want the old behavior)
320
+ * ```ts
321
+ * const { FlexibleError } = createTaggedError('FlexibleError')
322
+ * .withContext<Record<string, unknown> | undefined>()
323
+ * .withCause<AnyTaggedError | undefined>()
324
+ * ```
432
325
  */
433
- declare function defineError<TName extends `${string}Error`>(name: TName): ErrorBuilder<TName>;
326
+ declare function createTaggedError<TName extends `${string}Error`>(name: TName): ErrorBuilder<TName>;
434
327
  //#endregion
435
- export { AnyTaggedError, TaggedError, createTaggedError, defineError, extractErrorMessage };
328
+ export { AnyTaggedError, TaggedError, createTaggedError, extractErrorMessage };
436
329
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/utils.ts"],"sourcesContent":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/utils.ts"],"sourcesContent":[],"mappings":"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"}
@@ -63,30 +63,19 @@ function extractErrorMessage(error) {
63
63
  }
64
64
  return String(error);
65
65
  }
66
- function createTaggedError(name) {
67
- const errorConstructor = (input) => ({
68
- name,
69
- ...input
70
- });
71
- const errName = name.replace(/Error$/, "Err");
72
- const errConstructor = (input) => Err(errorConstructor(input));
73
- return {
74
- [name]: errorConstructor,
75
- [errName]: errConstructor
76
- };
77
- }
78
66
  /**
79
- * Defines a new tagged error type with a fluent builder API.
67
+ * Creates a new tagged error type with a fluent builder API.
80
68
  *
81
69
  * Returns an object containing:
82
70
  * - `{Name}Error`: Factory function that creates plain TaggedError objects
83
71
  * - `{Name}Err`: Factory function that creates Err-wrapped TaggedError objects
84
- * - `withContext<T>()`: Chain method to constrain context type
85
- * - `withCause<T>()`: Chain method to constrain cause type
72
+ * - `withContext<T>()`: Chain method to add context type
73
+ * - `withCause<T>()`: Chain method to add cause type
86
74
  *
87
- * **Default behavior (no chaining):**
88
- * - `context` is optional and accepts any `Record<string, unknown>`
89
- * - `cause` is optional and accepts any `AnyTaggedError`
75
+ * **Explicit Opt-In (Rust-inspired):**
76
+ * By default, errors only have `{ name, message }`. Context and cause must be
77
+ * explicitly added via `.withContext<T>()` and `.withCause<T>()`. This follows
78
+ * Rust's thiserror pattern where error properties are intentional decisions.
90
79
  *
91
80
  * **Optionality via type unions:**
92
81
  * Both `withContext` and `withCause` determine optionality based on whether
@@ -97,17 +86,17 @@ function createTaggedError(name) {
97
86
  * @template TName - The name of the error type (must end with "Error")
98
87
  * @param name - The name of the error type
99
88
  *
100
- * @example Simple error (flexible mode)
89
+ * @example Minimal error (no context, no cause)
101
90
  * ```ts
102
- * const { NetworkError, NetworkErr } = defineError('NetworkError')
91
+ * const { NetworkError, NetworkErr } = createTaggedError('NetworkError')
103
92
  *
104
93
  * NetworkError({ message: 'Connection failed' })
105
- * NetworkError({ message: 'Timeout', context: { url: 'https://...' } })
94
+ * // Error only has { name: 'NetworkError', message: 'Connection failed' }
106
95
  * ```
107
96
  *
108
97
  * @example Required context
109
98
  * ```ts
110
- * const { ApiError, ApiErr } = defineError('ApiError')
99
+ * const { ApiError, ApiErr } = createTaggedError('ApiError')
111
100
  * .withContext<{ endpoint: string; status: number }>()
112
101
  *
113
102
  * ApiError({ message: 'Failed', context: { endpoint: '/users', status: 500 } })
@@ -116,7 +105,7 @@ function createTaggedError(name) {
116
105
  *
117
106
  * @example Optional typed cause
118
107
  * ```ts
119
- * const { ServiceError } = defineError('ServiceError')
108
+ * const { ServiceError } = createTaggedError('ServiceError')
120
109
  * .withCause<DbError | CacheError | undefined>()
121
110
  *
122
111
  * ServiceError({ message: 'Failed' }) // OK
@@ -125,15 +114,22 @@ function createTaggedError(name) {
125
114
  *
126
115
  * @example Full example with both
127
116
  * ```ts
128
- * const { UserServiceError } = defineError('UserServiceError')
117
+ * const { UserServiceError } = createTaggedError('UserServiceError')
129
118
  * .withContext<{ userId: string }>()
130
119
  * .withCause<RepoError | undefined>()
131
120
  *
132
121
  * // Type extraction works
133
122
  * type UserServiceError = ReturnType<typeof UserServiceError>
134
123
  * ```
124
+ *
125
+ * @example Permissive mode (if you want the old behavior)
126
+ * ```ts
127
+ * const { FlexibleError } = createTaggedError('FlexibleError')
128
+ * .withContext<Record<string, unknown> | undefined>()
129
+ * .withCause<AnyTaggedError | undefined>()
130
+ * ```
135
131
  */
136
- function defineError(name) {
132
+ function createTaggedError(name) {
137
133
  const createBuilder = () => {
138
134
  const errorConstructor = (input) => ({
139
135
  name,
@@ -156,5 +152,5 @@ function defineError(name) {
156
152
  }
157
153
 
158
154
  //#endregion
159
- export { createTaggedError, defineError, extractErrorMessage };
155
+ export { createTaggedError, extractErrorMessage };
160
156
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["error: unknown","name: TName","input: {\n\t\tmessage: string;\n\t\tcontext?: TContext;\n\t\tcause?: TCause;\n\t}","input: ErrorInput<TContext, TCause>"],"sources":["../../src/error/utils.ts"],"sourcesContent":["import type { TaggedError, AnyTaggedError } from \"./types.js\";\nimport { Err } from \"../result/result.js\";\n\n/**\n * Extracts a readable error message from an unknown error value\n *\n * This utility is commonly used in mapErr functions when converting\n * unknown errors to typed error objects in the Result system.\n *\n * @param error - The unknown error to extract a message from\n * @returns A string representation of the error\n *\n * @example\n * ```ts\n * // With native Error\n * const error = new Error(\"Something went wrong\");\n * const message = extractErrorMessage(error); // \"Something went wrong\"\n *\n * // With string error\n * const stringError = \"String error\";\n * const message2 = extractErrorMessage(stringError); // \"String error\"\n *\n * // With object error\n * const unknownError = { code: 500, details: \"Server error\" };\n * const message3 = extractErrorMessage(unknownError); // '{\"code\":500,\"details\":\"Server error\"}'\n *\n * // Used in mapErr function\n * const result = await tryAsync({\n * try: () => riskyOperation(),\n * mapErr: (error) => Err({\n * name: \"NetworkError\",\n * message: extractErrorMessage(error),\n * context: { operation: \"riskyOperation\" },\n * cause: error,\n * }),\n * });\n * ```\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 *\n * @template T - An error type name that must end with \"Error\"\n * @returns The type name with \"Error\" replaced by \"Err\"\n *\n * @example\n * ```ts\n * type NetworkErr = ReplaceErrorWithErr<\"NetworkError\">; // \"NetworkErr\"\n * type ValidationErr = ReplaceErrorWithErr<\"ValidationError\">; // \"ValidationErr\"\n * ```\n */\ntype ReplaceErrorWithErr<T extends `${string}Error`> =\n\tT extends `${infer TBase}Error` ? `${TBase}Err` : never;\n\n// =============================================================================\n// Factory Return Types\n// =============================================================================\n\n/**\n * Return type when neither context nor cause are constrained (flexible mode).\n * Context and cause are optional with loose typing.\n */\ntype FlexibleFactories<TName extends `${string}Error`> = {\n\t[K in TName]: FlexibleErrorConstructor<K>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: FlexibleErrConstructor<TName>;\n};\n\n/**\n * Return type when context is fixed.\n * Context is required with exact type; cause is optional.\n */\ntype ContextFixedFactories<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown>,\n> = {\n\t[K in TName]: ContextFixedErrorConstructor<K, TContext>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: ContextFixedErrConstructor<TName, TContext>;\n};\n\n/**\n * Return type when both context and cause are fixed.\n * Context is required; cause is optional but constrained to specific type.\n */\ntype BothFixedFactories<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown>,\n\tTCause extends AnyTaggedError,\n> = {\n\t[K in TName]: BothFixedErrorConstructor<K, TContext, TCause>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: BothFixedErrConstructor<\n\t\tTName,\n\t\tTContext,\n\t\tTCause\n\t>;\n};\n\n// =============================================================================\n// Flexible Mode Constructor Types (SIMPLIFIED - no overloads)\n// =============================================================================\n\n/**\n * Creates plain TaggedError objects with flexible context and cause.\n * Single signature: context and cause are optional with loose typing.\n */\ntype FlexibleErrorConstructor<TName extends string> = (input: {\n\tmessage: string;\n\tcontext?: Record<string, unknown>;\n\tcause?: AnyTaggedError;\n}) => TaggedError<TName>;\n\n/**\n * Creates Err-wrapped TaggedError objects with flexible context and cause.\n * Single signature: context and cause are optional with loose typing.\n */\ntype FlexibleErrConstructor<TName extends string> = (input: {\n\tmessage: string;\n\tcontext?: Record<string, unknown>;\n\tcause?: AnyTaggedError;\n}) => Err<TaggedError<TName>>;\n\n// =============================================================================\n// Context-Fixed Mode Constructor Types (SIMPLIFIED - no overloads)\n// =============================================================================\n\n/**\n * Creates plain TaggedError objects with fixed context.\n * Single signature: context is required, cause is optional.\n */\ntype ContextFixedErrorConstructor<\n\tTName extends string,\n\tTContext extends Record<string, unknown>,\n> = (input: {\n\tmessage: string;\n\tcontext: TContext;\n\tcause?: AnyTaggedError;\n}) => TaggedError<TName, TContext>;\n\n/**\n * Creates Err-wrapped TaggedError objects with fixed context.\n * Single signature: context is required, cause is optional.\n */\ntype ContextFixedErrConstructor<\n\tTName extends string,\n\tTContext extends Record<string, unknown>,\n> = (input: {\n\tmessage: string;\n\tcontext: TContext;\n\tcause?: AnyTaggedError;\n}) => Err<TaggedError<TName, TContext>>;\n\n// =============================================================================\n// Both-Fixed Mode Constructor Types (SIMPLIFIED - no overloads)\n// =============================================================================\n\n/**\n * Creates plain TaggedError objects with both context and cause fixed.\n * Single signature: context is required, cause is optional but constrained.\n */\ntype BothFixedErrorConstructor<\n\tTName extends string,\n\tTContext extends Record<string, unknown>,\n\tTCause extends AnyTaggedError,\n> = (input: {\n\tmessage: string;\n\tcontext: TContext;\n\tcause?: TCause;\n}) => TaggedError<TName, TContext, TCause>;\n\n/**\n * Creates Err-wrapped TaggedError objects with both context and cause fixed.\n * Single signature: context is required, cause is optional but constrained.\n */\ntype BothFixedErrConstructor<\n\tTName extends string,\n\tTContext extends Record<string, unknown>,\n\tTCause extends AnyTaggedError,\n> = (input: {\n\tmessage: string;\n\tcontext: TContext;\n\tcause?: TCause;\n}) => Err<TaggedError<TName, TContext, TCause>>;\n\n// =============================================================================\n// Main Factory Function\n// =============================================================================\n\n/**\n * Creates two factory functions for building tagged errors with type-safe error chaining.\n *\n * @deprecated Use `defineError()` instead for a cleaner fluent API:\n * ```ts\n * // Before\n * const { FileError } = createTaggedError<'FileError', { path: string }>('FileError')\n *\n * // After\n * const { FileError } = defineError('FileError')\n * .withContext<{ path: string }>()\n * ```\n *\n * Given an error name like \"NetworkError\", this returns:\n * - `NetworkError`: Creates a plain TaggedError object\n * - `NetworkErr`: Creates a TaggedError object wrapped in an Err result\n *\n * **Three usage modes:**\n *\n * 1. **Flexible mode** (no type params): Context and cause are optional, loosely typed\n * 2. **Fixed context mode** (TContext specified): Context is required with exact shape\n * 3. **Both fixed mode** (TContext + TCause): Context required, cause constrained\n *\n * **ReturnType works correctly in all modes:**\n * ```ts\n * const { NetworkError } = createTaggedError('NetworkError');\n * type NetworkError = ReturnType<typeof NetworkError>;\n * // = TaggedError<'NetworkError'> with optional context/cause\n * ```\n *\n * @template TName - The name of the error type (must end with \"Error\")\n * @template TContext - Optional fixed context shape (makes context required)\n * @template TCause - Optional fixed cause type (constrains cause type if provided)\n * @param name - The name of the error type (must end with \"Error\")\n *\n * @example\n * ```ts\n * // Mode 1: Flexible - context and cause optional, loosely typed\n * const { NetworkError, NetworkErr } = createTaggedError('NetworkError');\n * NetworkError({ message: 'Connection failed' });\n * NetworkError({ message: 'Timeout', context: { url: 'https://...' } });\n * NetworkError({ message: 'Failed', cause: otherError });\n *\n * // Type annotation works with ReturnType:\n * type NetworkError = ReturnType<typeof NetworkError>;\n *\n * // Mode 2: Fixed context - context REQUIRED with exact shape\n * type BlobContext = { filename: string; code: 'INVALID' | 'TOO_LARGE' };\n * const { BlobError, BlobErr } = createTaggedError<'BlobError', BlobContext>('BlobError');\n * BlobError({ message: 'Invalid', context: { filename: 'x', code: 'INVALID' } });\n * // BlobError({ message: 'Error' }); // Type error - context required\n *\n * // Mode 3: Fixed context + cause - context required, cause constrained\n * const { ApiError, ApiErr } = createTaggedError<'ApiError', { endpoint: string }, NetworkError>('ApiError');\n * ApiError({ message: 'Failed', context: { endpoint: '/users' } });\n * ApiError({ message: 'Failed', context: { endpoint: '/users' }, cause: networkError });\n * ```\n */\n// Overload 1: Flexible (no type constraints)\nexport function createTaggedError<TName extends `${string}Error`>(\n\tname: TName,\n): FlexibleFactories<TName>;\n\n// Overload 2: Context fixed, cause flexible\nexport function createTaggedError<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown>,\n>(name: TName): ContextFixedFactories<TName, TContext>;\n\n// Overload 3: Both context and cause fixed\nexport function createTaggedError<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown>,\n\tTCause extends AnyTaggedError,\n>(name: TName): BothFixedFactories<TName, TContext, TCause>;\n\n// Implementation\nexport function createTaggedError<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown> = Record<string, unknown>,\n\tTCause extends AnyTaggedError = AnyTaggedError,\n>(name: TName): unknown {\n\tconst errorConstructor = (input: {\n\t\tmessage: string;\n\t\tcontext?: TContext;\n\t\tcause?: TCause;\n\t}) => ({ name, ...input });\n\n\tconst errName = name.replace(/Error$/, \"Err\") as ReplaceErrorWithErr<TName>;\n\tconst errConstructor = (input: {\n\t\tmessage: string;\n\t\tcontext?: TContext;\n\t\tcause?: TCause;\n\t}) => Err(errorConstructor(input));\n\n\treturn {\n\t\t[name]: errorConstructor,\n\t\t[errName]: errConstructor,\n\t};\n}\n\n// =============================================================================\n// Fluent API Types\n// =============================================================================\n\n/**\n * Helper type that determines optionality based on whether T includes undefined.\n * - If T includes undefined → property is optional\n * - If T does not include undefined → property is required\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 constructors with fluent API context/cause handling.\n */\ntype ErrorInput<\n\tTContext extends Record<string, unknown> | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = { message: string } & (TContext extends undefined\n\t? { context?: Record<string, unknown> }\n\t: OptionalIfUndefined<TContext, \"context\">) &\n\t(TCause extends undefined\n\t\t? { cause?: AnyTaggedError }\n\t\t: OptionalIfUndefined<TCause, \"cause\">);\n\n/**\n * The factories object returned by defineError and its builder methods.\n */\ntype ErrorFactories<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown> | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = {\n\t[K in TName]: (\n\t\tinput: ErrorInput<TContext, TCause>,\n\t) => TaggedError<TName, TContext, TCause>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: (\n\t\tinput: ErrorInput<TContext, TCause>,\n\t) => Err<TaggedError<TName, TContext, TCause>>;\n};\n\n/**\n * Builder interface for the fluent defineError API.\n * Provides chaining methods and the error factories.\n */\ntype ErrorBuilder<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown> | undefined = undefined,\n\tTCause extends AnyTaggedError | undefined = undefined,\n> = ErrorFactories<TName, TContext, TCause> & {\n\t/**\n\t * Constrains the context type for this error.\n\t *\n\t * Optionality is determined by whether the type includes `undefined`:\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\t * @typeParam T - The shape of the context object. Include `| undefined` to make optional.\n\t *\n\t * @example Required context\n\t * ```ts\n\t * const { FileError } = defineError('FileError')\n\t * .withContext<{ path: string }>()\n\t *\n\t * FileError({ message: 'Not found', context: { path: '/etc/config' } }) // OK\n\t * FileError({ message: 'Not found' }) // Type error: context required\n\t * ```\n\t *\n\t * @example Optional but typed context\n\t * ```ts\n\t * const { LogError } = defineError('LogError')\n\t * .withContext<{ file: string; line: number } | undefined>()\n\t *\n\t * LogError({ message: 'Parse error' }) // OK\n\t * LogError({ message: 'Parse error', context: { file: 'app.ts', line: 42 } }) // OK\n\t * ```\n\t */\n\twithContext<T extends Record<string, unknown> | undefined>(): ErrorBuilder<\n\t\tTName,\n\t\tT,\n\t\tTCause\n\t>;\n\n\t/**\n\t * Constrains the cause type for this error.\n\t *\n\t * Optionality is determined by whether the type includes `undefined`:\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\t * Since cause is typically optional, include `| undefined` in most cases.\n\t *\n\t * @typeParam T - The allowed cause type(s). Include `| undefined` to make optional.\n\t *\n\t * @example Optional typed cause (common)\n\t * ```ts\n\t * const { ServiceError } = defineError('ServiceError')\n\t * .withCause<DbError | CacheError | undefined>()\n\t *\n\t * ServiceError({ message: 'Failed' }) // OK\n\t * ServiceError({ message: 'Failed', cause: dbError }) // OK\n\t * ```\n\t *\n\t * @example Required cause (for wrapper errors)\n\t * ```ts\n\t * const { UnhandledError } = defineError('UnhandledError')\n\t * .withCause<AnyTaggedError>()\n\t *\n\t * UnhandledError({ message: 'Unexpected', cause: originalError }) // OK\n\t * UnhandledError({ message: 'Unexpected' }) // Type error: cause required\n\t * ```\n\t */\n\twithCause<T extends AnyTaggedError | undefined>(): ErrorBuilder<\n\t\tTName,\n\t\tTContext,\n\t\tT\n\t>;\n};\n\n// =============================================================================\n// Fluent API Implementation\n// =============================================================================\n\n/**\n * Defines a new tagged error type with a fluent builder API.\n *\n * Returns an object containing:\n * - `{Name}Error`: Factory function that creates plain TaggedError objects\n * - `{Name}Err`: Factory function that creates Err-wrapped TaggedError objects\n * - `withContext<T>()`: Chain method to constrain context type\n * - `withCause<T>()`: Chain method to constrain cause type\n *\n * **Default behavior (no chaining):**\n * - `context` is optional and accepts any `Record<string, unknown>`\n * - `cause` is optional and accepts any `AnyTaggedError`\n *\n * **Optionality via type unions:**\n * Both `withContext` and `withCause` determine optionality based on whether\n * the type includes `undefined`:\n * - `T` without undefined → property is required\n * - `T | undefined` → property is optional but typed when provided\n *\n * @template TName - The name of the error type (must end with \"Error\")\n * @param name - The name of the error type\n *\n * @example Simple error (flexible mode)\n * ```ts\n * const { NetworkError, NetworkErr } = defineError('NetworkError')\n *\n * NetworkError({ message: 'Connection failed' })\n * NetworkError({ message: 'Timeout', context: { url: 'https://...' } })\n * ```\n *\n * @example Required context\n * ```ts\n * const { ApiError, ApiErr } = defineError('ApiError')\n * .withContext<{ endpoint: string; status: number }>()\n *\n * ApiError({ message: 'Failed', context: { endpoint: '/users', status: 500 } })\n * // ApiError({ message: 'Failed' }) // Type error: context required\n * ```\n *\n * @example Optional typed cause\n * ```ts\n * const { ServiceError } = defineError('ServiceError')\n * .withCause<DbError | CacheError | undefined>()\n *\n * ServiceError({ message: 'Failed' }) // OK\n * ServiceError({ message: 'Failed', cause: dbError }) // OK, typed\n * ```\n *\n * @example Full example with both\n * ```ts\n * const { UserServiceError } = defineError('UserServiceError')\n * .withContext<{ userId: string }>()\n * .withCause<RepoError | undefined>()\n *\n * // Type extraction works\n * type UserServiceError = ReturnType<typeof UserServiceError>\n * ```\n */\nexport function defineError<TName extends `${string}Error`>(\n\tname: TName,\n): ErrorBuilder<TName> {\n\tconst createBuilder = <\n\t\tTContext extends Record<string, unknown> | undefined = undefined,\n\t\tTCause extends AnyTaggedError | undefined = undefined,\n\t>(): ErrorBuilder<TName, TContext, TCause> => {\n\t\tconst errorConstructor = (input: ErrorInput<TContext, TCause>) =>\n\t\t\t({ name, ...input }) as unknown as TaggedError<TName, TContext, TCause>;\n\n\t\tconst errName = name.replace(\n\t\t\t/Error$/,\n\t\t\t\"Err\",\n\t\t) as ReplaceErrorWithErr<TName>;\n\t\tconst errConstructor = (input: ErrorInput<TContext, TCause>) =>\n\t\t\tErr(errorConstructor(input));\n\n\t\treturn {\n\t\t\t[name]: errorConstructor,\n\t\t\t[errName]: errConstructor,\n\t\t\twithContext<T extends Record<string, unknown> | undefined>() {\n\t\t\t\treturn createBuilder<T, TCause>();\n\t\t\t},\n\t\t\twithCause<T extends AnyTaggedError | undefined>() {\n\t\t\t\treturn createBuilder<TContext, T>();\n\t\t\t},\n\t\t} as ErrorBuilder<TName, TContext, TCause>;\n\t};\n\n\treturn createBuilder();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,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;AAqOD,SAAgB,kBAIdC,MAAsB;CACvB,MAAM,mBAAmB,CAACC,WAInB;EAAE;EAAM,GAAG;CAAO;CAEzB,MAAM,UAAU,KAAK,QAAQ,UAAU,MAAM;CAC7C,MAAM,iBAAiB,CAACA,UAIlB,IAAI,iBAAiB,MAAM,CAAC;AAElC,QAAO;GACL,OAAO;GACP,UAAU;CACX;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyLD,SAAgB,YACfD,MACsB;CACtB,MAAM,gBAAgB,MAGwB;EAC7C,MAAM,mBAAmB,CAACE,WACxB;GAAE;GAAM,GAAG;EAAO;EAEpB,MAAM,UAAU,KAAK,QACpB,UACA,MACA;EACD,MAAM,iBAAiB,CAACA,UACvB,IAAI,iBAAiB,MAAM,CAAC;AAE7B,SAAO;IACL,OAAO;IACP,UAAU;GACX,cAA6D;AAC5D,WAAO,eAA0B;GACjC;GACD,YAAkD;AACjD,WAAO,eAA4B;GACnC;EACD;CACD;AAED,QAAO,eAAe;AACtB"}
1
+ {"version":3,"file":"index.js","names":["error: unknown","name: TName","input: ErrorInput<TContext, TCause>"],"sources":["../../src/error/utils.ts"],"sourcesContent":["import type { TaggedError, AnyTaggedError } from \"./types.js\";\nimport { Err } from \"../result/result.js\";\n\n/**\n * Extracts a readable error message from an unknown error value\n *\n * This utility is commonly used in mapErr functions when converting\n * unknown errors to typed error objects in the Result system.\n *\n * @param error - The unknown error to extract a message from\n * @returns A string representation of the error\n *\n * @example\n * ```ts\n * // With native Error\n * const error = new Error(\"Something went wrong\");\n * const message = extractErrorMessage(error); // \"Something went wrong\"\n *\n * // With string error\n * const stringError = \"String error\";\n * const message2 = extractErrorMessage(stringError); // \"String error\"\n *\n * // With object error\n * const unknownError = { code: 500, details: \"Server error\" };\n * const message3 = extractErrorMessage(unknownError); // '{\"code\":500,\"details\":\"Server error\"}'\n *\n * // Used in mapErr function\n * const result = await tryAsync({\n * try: () => riskyOperation(),\n * mapErr: (error) => Err({\n * name: \"NetworkError\",\n * message: extractErrorMessage(error),\n * context: { operation: \"riskyOperation\" },\n * cause: error,\n * }),\n * });\n * ```\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 *\n * @template T - An error type name that must end with \"Error\"\n * @returns The type name with \"Error\" replaced by \"Err\"\n *\n * @example\n * ```ts\n * type NetworkErr = ReplaceErrorWithErr<\"NetworkError\">; // \"NetworkErr\"\n * type ValidationErr = ReplaceErrorWithErr<\"ValidationError\">; // \"ValidationErr\"\n * ```\n */\ntype ReplaceErrorWithErr<T extends `${string}Error`> =\n\tT extends `${infer TBase}Error` ? `${TBase}Err` : never;\n\n// =============================================================================\n// Fluent API Types\n// =============================================================================\n\n/**\n * Helper type that determines optionality based on whether T includes undefined.\n * - If T includes undefined → property is optional\n * - If T does not include undefined → property is required\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 constructors with fluent API context/cause handling.\n *\n * Follows explicit opt-in philosophy:\n * - When TContext/TCause is undefined: property doesn't exist\n * - When TContext/TCause includes undefined: property is optional but typed\n * - When TContext/TCause is a specific type: property is required\n */\ntype ErrorInput<\n\tTContext extends Record<string, unknown> | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = { message: string } & (TContext extends undefined\n\t? {}\n\t: OptionalIfUndefined<TContext, \"context\">) &\n\t(TCause extends undefined\n\t\t? {}\n\t\t: OptionalIfUndefined<TCause, \"cause\">);\n\n/**\n * The factories object returned by createTaggedError and its builder methods.\n */\ntype ErrorFactories<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown> | undefined,\n\tTCause extends AnyTaggedError | undefined,\n> = {\n\t[K in TName]: (\n\t\tinput: ErrorInput<TContext, TCause>,\n\t) => TaggedError<TName, TContext, TCause>;\n} & {\n\t[K in ReplaceErrorWithErr<TName>]: (\n\t\tinput: ErrorInput<TContext, TCause>,\n\t) => Err<TaggedError<TName, TContext, TCause>>;\n};\n\n/**\n * Builder interface for the fluent createTaggedError API.\n * Provides chaining methods and the error factories.\n */\ntype ErrorBuilder<\n\tTName extends `${string}Error`,\n\tTContext extends Record<string, unknown> | undefined = undefined,\n\tTCause extends AnyTaggedError | undefined = undefined,\n> = ErrorFactories<TName, TContext, TCause> & {\n\t/**\n\t * Constrains the context type for this error.\n\t *\n\t * Optionality is determined by whether the type includes `undefined`:\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\t * @typeParam T - The shape of the context object. Include `| undefined` to make optional.\n\t *\n\t * @example Required context\n\t * ```ts\n\t * const { FileError } = createTaggedError('FileError')\n\t * .withContext<{ path: string }>()\n\t *\n\t * FileError({ message: 'Not found', context: { path: '/etc/config' } }) // OK\n\t * FileError({ message: 'Not found' }) // Type error: context required\n\t * ```\n\t *\n\t * @example Optional but typed context\n\t * ```ts\n\t * const { LogError } = createTaggedError('LogError')\n\t * .withContext<{ file: string; line: number } | undefined>()\n\t *\n\t * LogError({ message: 'Parse error' }) // OK\n\t * LogError({ message: 'Parse error', context: { file: 'app.ts', line: 42 } }) // OK\n\t * ```\n\t *\n\t * @example Default (no generic): permissive optional context\n\t * ```ts\n\t * const { FlexError } = createTaggedError('FlexError')\n\t * .withContext() // Defaults to Record<string, unknown> | undefined\n\t *\n\t * FlexError({ message: 'Error' }) // OK - context is optional\n\t * FlexError({ message: 'Error', context: { anything: 'works' } }) // OK\n\t * ```\n\t */\n\twithContext<\n\t\tT extends Record<string, unknown> | undefined = Record<string, unknown> | undefined,\n\t>(): ErrorBuilder<TName, T, TCause>;\n\n\t/**\n\t * Constrains the cause type for this error.\n\t *\n\t * Optionality is determined by whether the type includes `undefined`:\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\t * Since cause is typically optional, include `| undefined` in most cases.\n\t *\n\t * @typeParam T - The allowed cause type(s). Include `| undefined` to make optional.\n\t *\n\t * @example Optional typed cause (common)\n\t * ```ts\n\t * const { ServiceError } = createTaggedError('ServiceError')\n\t * .withCause<DbError | CacheError | undefined>()\n\t *\n\t * ServiceError({ message: 'Failed' }) // OK\n\t * ServiceError({ message: 'Failed', cause: dbError }) // OK\n\t * ```\n\t *\n\t * @example Required cause (for wrapper errors)\n\t * ```ts\n\t * const { UnhandledError } = createTaggedError('UnhandledError')\n\t * .withCause<AnyTaggedError>()\n\t *\n\t * UnhandledError({ message: 'Unexpected', cause: originalError }) // OK\n\t * UnhandledError({ message: 'Unexpected' }) // Type error: cause required\n\t * ```\n\t *\n\t * @example Default (no generic): permissive optional cause\n\t * ```ts\n\t * const { FlexError } = createTaggedError('FlexError')\n\t * .withCause() // Defaults to AnyTaggedError | undefined\n\t *\n\t * FlexError({ message: 'Error' }) // OK - cause is optional\n\t * FlexError({ message: 'Error', cause: anyTaggedError }) // OK\n\t * ```\n\t */\n\twithCause<\n\t\tT extends AnyTaggedError | undefined = AnyTaggedError | undefined,\n\t>(): ErrorBuilder<TName, TContext, T>;\n};\n\n// =============================================================================\n// Fluent API Implementation\n// =============================================================================\n\n/**\n * Creates a new tagged error type with a fluent builder API.\n *\n * Returns an object containing:\n * - `{Name}Error`: Factory function that creates plain TaggedError objects\n * - `{Name}Err`: Factory function that creates Err-wrapped TaggedError objects\n * - `withContext<T>()`: Chain method to add context type\n * - `withCause<T>()`: Chain method to add cause type\n *\n * **Explicit Opt-In (Rust-inspired):**\n * By default, errors only have `{ name, message }`. Context and cause must be\n * explicitly added via `.withContext<T>()` and `.withCause<T>()`. This follows\n * Rust's thiserror pattern where error properties are intentional decisions.\n *\n * **Optionality via type unions:**\n * Both `withContext` and `withCause` determine optionality based on whether\n * the type includes `undefined`:\n * - `T` without undefined → property is required\n * - `T | undefined` → property is optional but typed when provided\n *\n * @template TName - The name of the error type (must end with \"Error\")\n * @param name - The name of the error type\n *\n * @example Minimal error (no context, no cause)\n * ```ts\n * const { NetworkError, NetworkErr } = createTaggedError('NetworkError')\n *\n * NetworkError({ message: 'Connection failed' })\n * // Error only has { name: 'NetworkError', message: 'Connection failed' }\n * ```\n *\n * @example Required context\n * ```ts\n * const { ApiError, ApiErr } = createTaggedError('ApiError')\n * .withContext<{ endpoint: string; status: number }>()\n *\n * ApiError({ message: 'Failed', context: { endpoint: '/users', status: 500 } })\n * // ApiError({ message: 'Failed' }) // Type error: context required\n * ```\n *\n * @example Optional typed cause\n * ```ts\n * const { ServiceError } = createTaggedError('ServiceError')\n * .withCause<DbError | CacheError | undefined>()\n *\n * ServiceError({ message: 'Failed' }) // OK\n * ServiceError({ message: 'Failed', cause: dbError }) // OK, typed\n * ```\n *\n * @example Full example with both\n * ```ts\n * const { UserServiceError } = createTaggedError('UserServiceError')\n * .withContext<{ userId: string }>()\n * .withCause<RepoError | undefined>()\n *\n * // Type extraction works\n * type UserServiceError = ReturnType<typeof UserServiceError>\n * ```\n *\n * @example Permissive mode (if you want the old behavior)\n * ```ts\n * const { FlexibleError } = createTaggedError('FlexibleError')\n * .withContext<Record<string, unknown> | undefined>()\n * .withCause<AnyTaggedError | undefined>()\n * ```\n */\nexport function createTaggedError<TName extends `${string}Error`>(\n\tname: TName,\n): ErrorBuilder<TName> {\n\tconst createBuilder = <\n\t\tTContext extends Record<string, unknown> | undefined = undefined,\n\t\tTCause extends AnyTaggedError | undefined = undefined,\n\t>(): ErrorBuilder<TName, TContext, TCause> => {\n\t\tconst errorConstructor = (input: ErrorInput<TContext, TCause>) =>\n\t\t\t({ name, ...input }) as unknown as TaggedError<TName, TContext, TCause>;\n\n\t\tconst errName = name.replace(\n\t\t\t/Error$/,\n\t\t\t\"Err\",\n\t\t) as ReplaceErrorWithErr<TName>;\n\t\tconst errConstructor = (input: ErrorInput<TContext, TCause>) =>\n\t\t\tErr(errorConstructor(input));\n\n\t\treturn {\n\t\t\t[name]: errorConstructor,\n\t\t\t[errName]: errConstructor,\n\t\t\twithContext<\n\t\t\t\tT extends Record<string, unknown> | undefined = Record<string, unknown> | 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} as ErrorBuilder<TName, TContext, TCause>;\n\t};\n\n\treturn createBuilder();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmOD,SAAgB,kBACfC,MACsB;CACtB,MAAM,gBAAgB,MAGwB;EAC7C,MAAM,mBAAmB,CAACC,WACxB;GAAE;GAAM,GAAG;EAAO;EAEpB,MAAM,UAAU,KAAK,QACpB,UACA,MACA;EACD,MAAM,iBAAiB,CAACA,UACvB,IAAI,iBAAiB,MAAM,CAAC;AAE7B,SAAO;IACL,OAAO;IACP,UAAU;GACX,cAEI;AACH,WAAO,eAA0B;GACjC;GACD,YAEI;AACH,WAAO,eAA4B;GACnC;EACD;CACD;AAED,QAAO,eAAe;AACtB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wellcrafted",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Delightful TypeScript patterns for elegant, type-safe applications",
5
5
  "type": "module",
6
6
  "files": [