wellcrafted 0.29.0 → 0.31.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 +34 -7
- package/dist/brand.d.ts +85 -17
- package/dist/brand.d.ts.map +1 -1
- package/dist/error/index.d.ts +72 -241
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +38 -91
- package/dist/error/index.js.map +1 -1
- package/dist/query/index.d.ts +9 -6
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +12 -11
- package/dist/query/index.js.map +1 -1
- package/dist/result/index.js +2 -2
- package/dist/{result-DfuKgZo9.js → result-0QjbC3Hw.js} +2 -2
- package/dist/{result-DfuKgZo9.js.map → result-0QjbC3Hw.js.map} +1 -1
- package/dist/{result-B1iWFqM9.js → result-DnOm5ds5.js} +1 -3
- package/dist/result-DnOm5ds5.js.map +1 -0
- package/dist/result-DolxQXIZ.d.ts.map +1 -1
- package/dist/standard-schema/index.d.ts +371 -0
- package/dist/standard-schema/index.d.ts.map +1 -0
- package/dist/standard-schema/index.js +344 -0
- package/dist/standard-schema/index.js.map +1 -0
- package/package.json +13 -5
- package/dist/result-B1iWFqM9.js.map +0 -1
package/README.md
CHANGED
|
@@ -46,8 +46,8 @@ function divide(a: number, b: number): Result<number, string> {
|
|
|
46
46
|
### 🏷️ Brand Types
|
|
47
47
|
Create distinct types from primitives
|
|
48
48
|
```typescript
|
|
49
|
-
type UserId = Brand<
|
|
50
|
-
type OrderId = Brand<
|
|
49
|
+
type UserId = string & Brand<"UserId">;
|
|
50
|
+
type OrderId = string & Brand<"OrderId">;
|
|
51
51
|
|
|
52
52
|
// TypeScript prevents mixing them up!
|
|
53
53
|
function getUser(id: UserId) { /* ... */ }
|
|
@@ -84,11 +84,13 @@ const userQuery = defineQuery({
|
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
// Use reactively in components with automatic state management
|
|
87
|
-
|
|
87
|
+
// Svelte 5 requires accessor function; React uses options directly
|
|
88
|
+
const query = createQuery(() => userQuery.options); // Svelte
|
|
88
89
|
// query.data, query.error, query.isPending all managed automatically
|
|
89
90
|
|
|
90
91
|
// Or use imperatively for direct execution (perfect for event handlers)
|
|
91
92
|
const { data, error } = await userQuery.fetch();
|
|
93
|
+
// Or shorthand: await userQuery() (same as .ensure())
|
|
92
94
|
if (error) {
|
|
93
95
|
showErrorToast(error.message);
|
|
94
96
|
return;
|
|
@@ -336,12 +338,13 @@ export const recorder = {
|
|
|
336
338
|
};
|
|
337
339
|
|
|
338
340
|
// 3. Component Usage - Choose reactive or imperative based on needs
|
|
339
|
-
// Reactive: Automatic state management
|
|
340
|
-
const recorderState = createQuery(recorder.getRecorderState.options
|
|
341
|
+
// Reactive: Automatic state management (Svelte 5 requires accessor function)
|
|
342
|
+
const recorderState = createQuery(() => recorder.getRecorderState.options);
|
|
341
343
|
|
|
342
344
|
// Imperative: Direct execution for event handlers
|
|
343
345
|
async function handleStartRecording() {
|
|
344
346
|
const { error } = await recorder.startRecording.execute();
|
|
347
|
+
// Or shorthand: await recorder.startRecording()
|
|
345
348
|
if (error) {
|
|
346
349
|
showToast(error.title, { description: error.description });
|
|
347
350
|
}
|
|
@@ -623,8 +626,8 @@ For comprehensive examples, service layer patterns, framework integrations, and
|
|
|
623
626
|
|
|
624
627
|
### Query Functions
|
|
625
628
|
- **`createQueryFactories(client)`** - Create query/mutation factories for TanStack Query
|
|
626
|
-
- **`defineQuery(options)`** - Define a query with dual interface (`.options
|
|
627
|
-
- **`defineMutation(options)`** - Define a mutation with dual interface (`.options
|
|
629
|
+
- **`defineQuery(options)`** - Define a query with dual interface (`.options` + callable/`.ensure()`/`.fetch()`)
|
|
630
|
+
- **`defineMutation(options)`** - Define a mutation with dual interface (`.options` + callable/`.execute()`)
|
|
628
631
|
|
|
629
632
|
### Error Functions
|
|
630
633
|
- **`createTaggedError(name)`** - Creates error factory functions with fluent API
|
|
@@ -646,6 +649,30 @@ For comprehensive examples, service layer patterns, framework integrations, and
|
|
|
646
649
|
- **`UnwrapOk<R>`** - Extract success value type from Result
|
|
647
650
|
- **`UnwrapErr<R>`** - Extract error value type from Result
|
|
648
651
|
|
|
652
|
+
## Development Setup
|
|
653
|
+
|
|
654
|
+
### Peer Directory Requirement
|
|
655
|
+
|
|
656
|
+
Wellcrafted shares AI agent skills (`.agents/skills/`, `.claude/skills/`) with the [Epicenter](https://github.com/EpicenterHQ/epicenter) repo via relative symlinks. Epicenter is the source of truth for skill definitions — they're authored and maintained there, and wellcrafted consumes them to stay in sync.
|
|
657
|
+
|
|
658
|
+
**Both repos must be sibling directories under the same parent:**
|
|
659
|
+
|
|
660
|
+
```
|
|
661
|
+
Code/
|
|
662
|
+
├── epicenter/ # Source of truth for skills
|
|
663
|
+
│ └── .agents/skills/
|
|
664
|
+
└── wellcrafted/ # Symlinks to epicenter
|
|
665
|
+
├── .agents/skills/<name> → ../../../epicenter/.agents/skills/<name>
|
|
666
|
+
└── .claude/skills/<name> → ../../.agents/skills/<name>
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
If symlinks appear broken after cloning, ensure epicenter is cloned alongside wellcrafted:
|
|
670
|
+
|
|
671
|
+
```bash
|
|
672
|
+
cd "$(git rev-parse --show-toplevel)/.."
|
|
673
|
+
git clone https://github.com/EpicenterHQ/epicenter.git
|
|
674
|
+
```
|
|
675
|
+
|
|
649
676
|
## License
|
|
650
677
|
|
|
651
678
|
MIT
|
package/dist/brand.d.ts
CHANGED
|
@@ -6,35 +6,103 @@ declare const brand: unique symbol;
|
|
|
6
6
|
/**
|
|
7
7
|
* Creates a brand type for nominal typing in TypeScript.
|
|
8
8
|
*
|
|
9
|
-
* Branded types
|
|
9
|
+
* Branded types create distinct types from primitive types, preventing
|
|
10
10
|
* accidental mixing of values that should be semantically different.
|
|
11
11
|
*
|
|
12
|
+
* ## Why wellcrafted Brand?
|
|
13
|
+
*
|
|
14
|
+
* **Composable** — Brands stack to create hierarchical type relationships.
|
|
15
|
+
* Child types are assignable to parent types, but not vice versa. Multiple
|
|
16
|
+
* inheritance works via intersection. This is possible because of the nested
|
|
17
|
+
* object structure `{ [brand]: { [K in T]: true } }` — when brands intersect,
|
|
18
|
+
* their properties merge instead of conflicting.
|
|
19
|
+
*
|
|
20
|
+
* **Framework-agnostic** — Unlike Zod's `.brand()`, ArkType's `.brand()`, or
|
|
21
|
+
* Valibot's `v.brand()` — which each produce library-specific branded types —
|
|
22
|
+
* wellcrafted's `Brand<T>` is a pure type utility with zero runtime footprint.
|
|
23
|
+
* Define your branded type once, then plug it into any runtime validator.
|
|
24
|
+
* Switch validation libraries without touching your type definitions.
|
|
25
|
+
*
|
|
26
|
+
* **Dual-declaration friendly** — TypeScript has two parallel namespaces: types
|
|
27
|
+
* and values. You can use the same PascalCase name for both the branded type and
|
|
28
|
+
* its runtime validator. JSDoc written on the type shows up everywhere the name
|
|
29
|
+
* appears — function signatures, schema definitions, and imports — giving you a
|
|
30
|
+
* single hover experience across your entire codebase.
|
|
31
|
+
*
|
|
12
32
|
* @template T - A string literal type that serves as the brand identifier
|
|
13
33
|
*
|
|
14
|
-
* @example
|
|
34
|
+
* @example Single brand — preventing ID mix-ups
|
|
15
35
|
* ```typescript
|
|
16
|
-
*
|
|
17
|
-
* type
|
|
36
|
+
* type UserId = string & Brand<"UserId">;
|
|
37
|
+
* type OrderId = string & Brand<"OrderId">;
|
|
18
38
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* }
|
|
39
|
+
* const userId: UserId = "user-123" as UserId;
|
|
40
|
+
* const orderId: OrderId = userId; // ❌ Type error
|
|
41
|
+
* ```
|
|
23
42
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
43
|
+
* @example Hierarchical brands — child assignable to parent
|
|
44
|
+
* ```typescript
|
|
45
|
+
* type AbsolutePath = string & Brand<"AbsolutePath">;
|
|
46
|
+
* type ConfigPath = AbsolutePath & Brand<"ConfigPath">;
|
|
27
47
|
*
|
|
28
|
-
*
|
|
29
|
-
* const
|
|
30
|
-
* const
|
|
48
|
+
* declare const configPath: ConfigPath;
|
|
49
|
+
* const abs: AbsolutePath = configPath; // ✅ Child assignable to parent
|
|
50
|
+
* const cfg: ConfigPath = abs; // ❌ Parent not assignable to child
|
|
51
|
+
* ```
|
|
31
52
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
53
|
+
* @example Multiple inheritance
|
|
54
|
+
* ```typescript
|
|
55
|
+
* type Serializable = unknown & Brand<"Serializable">;
|
|
56
|
+
* type Validated = unknown & Brand<"Validated">;
|
|
57
|
+
* type SafeData = Serializable & Validated & Brand<"SafeData">;
|
|
58
|
+
*
|
|
59
|
+
* // SafeData is assignable to both Serializable and Validated
|
|
34
60
|
* ```
|
|
61
|
+
*
|
|
62
|
+
* @example Dual-declaration — same name for type and runtime validator
|
|
63
|
+
*
|
|
64
|
+
* TypeScript resolves the name from context: type position = branded type,
|
|
65
|
+
* value position = runtime validator. One name, zero ambiguity.
|
|
66
|
+
*
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // Type-only brand (no runtime validation needed)
|
|
69
|
+
* type Guid = string & Brand<"Guid">;
|
|
70
|
+
*
|
|
71
|
+
* // Dual-declaration: type + runtime validator share the same name.
|
|
72
|
+
* // Hover over FileId anywhere — IDE shows the same JSDoc whether
|
|
73
|
+
* // it appears as a type annotation or a runtime schema.
|
|
74
|
+
* type FileId = Guid & Brand<"FileId">;
|
|
75
|
+
* const FileId = type("string").pipe((s): FileId => s as FileId);
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @example Framework-agnostic — same Brand, any validator
|
|
79
|
+
*
|
|
80
|
+
* Define the type once with `Brand<T>`, then create runtime validators
|
|
81
|
+
* with whichever library you prefer. Your branded types are never locked
|
|
82
|
+
* to a specific validation library.
|
|
83
|
+
*
|
|
84
|
+
* ```typescript
|
|
85
|
+
* import { type } from "arktype";
|
|
86
|
+
* import { z } from "zod";
|
|
87
|
+
* import * as v from "valibot";
|
|
88
|
+
*
|
|
89
|
+
* // Define the type ONCE — it's just a type, no library dependency
|
|
90
|
+
* type FileId = string & Brand<"FileId">;
|
|
91
|
+
*
|
|
92
|
+
* // ArkType
|
|
93
|
+
* const FileId = type("string").pipe((s): FileId => s as FileId);
|
|
94
|
+
*
|
|
95
|
+
* // Zod
|
|
96
|
+
* const FileId = z.string().transform((s): FileId => s as FileId);
|
|
97
|
+
*
|
|
98
|
+
* // Valibot
|
|
99
|
+
* const FileId = v.pipe(v.string(), v.transform((s): FileId => s as FileId));
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @see {@link https://wellcrafted.dev/integrations/validation-libraries | Using Brand with Validation Libraries}
|
|
35
103
|
*/
|
|
36
104
|
type Brand<T extends string> = {
|
|
37
|
-
[brand]: T;
|
|
105
|
+
[brand]: { [K in T]: true };
|
|
38
106
|
};
|
|
39
107
|
//#endregion
|
|
40
108
|
export { Brand };
|
package/dist/brand.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brand.d.ts","names":[],"sources":["../src/brand.ts"],"sourcesContent":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"brand.d.ts","names":[],"sources":["../src/brand.ts"],"sourcesContent":[],"mappings":";;;AAGkC;cAApB,KAoGJ,EAAA,OAAA,MAAA;;;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KADF;GACH,KAAA,WAAgB"}
|
package/dist/error/index.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { Err } from "../result-DolxQXIZ.js";
|
|
2
2
|
|
|
3
3
|
//#region src/error/types.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* JSON-serializable value types for error context.
|
|
7
|
+
* Ensures all error data can be safely serialized via JSON.stringify.
|
|
8
|
+
*/
|
|
9
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
10
|
+
[key: string]: JsonValue;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* JSON-serializable object type for error context.
|
|
14
|
+
*/
|
|
15
|
+
type JsonObject = Record<string, JsonValue>;
|
|
4
16
|
/**
|
|
5
17
|
* Base type for any tagged error, used as a constraint for cause parameters.
|
|
6
18
|
*/
|
|
@@ -13,10 +25,8 @@ type AnyTaggedError = {
|
|
|
13
25
|
* - When TContext is undefined (default): NO context property (explicit opt-in)
|
|
14
26
|
* - When TContext includes undefined (e.g., `{ foo: string } | undefined`): context is OPTIONAL but typed
|
|
15
27
|
* - When TContext is a specific type without undefined: context is REQUIRED with that exact type
|
|
16
|
-
*
|
|
17
|
-
* This follows Rust's explicit error philosophy: context must be explicitly added via .withContext<T>().
|
|
18
28
|
*/
|
|
19
|
-
type WithContext<TContext> = [TContext] extends [undefined] ?
|
|
29
|
+
type WithContext<TContext> = [TContext] extends [undefined] ? Record<never, never> : [undefined] extends [TContext] ? {
|
|
20
30
|
context?: Exclude<TContext, undefined>;
|
|
21
31
|
} : {
|
|
22
32
|
context: TContext;
|
|
@@ -26,80 +36,21 @@ type WithContext<TContext> = [TContext] extends [undefined] ? {} : [undefined] e
|
|
|
26
36
|
* - When TCause is undefined (default): NO cause property (explicit opt-in)
|
|
27
37
|
* - When TCause includes undefined (e.g., `NetworkError | undefined`): cause is OPTIONAL, constrained
|
|
28
38
|
* - When TCause is a specific type without undefined: cause is REQUIRED
|
|
29
|
-
*
|
|
30
|
-
* This follows Rust's explicit error philosophy: cause must be explicitly added via .withCause<T>().
|
|
31
|
-
* Using brackets to prevent distributive conditional behavior with union types.
|
|
32
39
|
*/
|
|
33
|
-
type WithCause<TCause> = [TCause] extends [undefined] ?
|
|
40
|
+
type WithCause<TCause> = [TCause] extends [undefined] ? Record<never, never> : [undefined] extends [TCause] ? {
|
|
34
41
|
cause?: Exclude<TCause, undefined>;
|
|
35
42
|
} : {
|
|
36
43
|
cause: TCause;
|
|
37
44
|
};
|
|
38
45
|
/**
|
|
39
|
-
*
|
|
46
|
+
* A tagged error type for type-safe error handling.
|
|
40
47
|
* Uses the `name` property as a discriminator for tagged unions.
|
|
41
48
|
*
|
|
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.
|
|
46
|
-
*
|
|
47
|
-
* **Type Parameter Behavior:**
|
|
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
|
|
54
|
-
*
|
|
55
49
|
* @template TName - The error name (discriminator for tagged unions)
|
|
56
50
|
* @template TContext - Additional context data for the error (default: undefined = no context)
|
|
57
51
|
* @template TCause - The type of error that caused this error (default: undefined = no cause)
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* ```ts
|
|
61
|
-
* // Minimal error (no context, no cause)
|
|
62
|
-
* type ValidationError = TaggedError<"ValidationError">;
|
|
63
|
-
* const validationError: ValidationError = {
|
|
64
|
-
* name: "ValidationError",
|
|
65
|
-
* message: "Input is required"
|
|
66
|
-
* };
|
|
67
|
-
* // validationError only has name and message
|
|
68
|
-
*
|
|
69
|
-
* // Error with required context
|
|
70
|
-
* type NetworkError = TaggedError<"NetworkError", { host: string; port: number }>;
|
|
71
|
-
* const networkError: NetworkError = {
|
|
72
|
-
* name: "NetworkError",
|
|
73
|
-
* message: "Socket timeout",
|
|
74
|
-
* context: { host: "db.example.com", port: 5432 } // Required!
|
|
75
|
-
* };
|
|
76
|
-
*
|
|
77
|
-
* // Error with OPTIONAL but TYPED context (union with undefined)
|
|
78
|
-
* type LogError = TaggedError<"LogError", { file: string; line: number } | undefined>;
|
|
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
|
|
81
|
-
*
|
|
82
|
-
* // Error with required context and optional cause
|
|
83
|
-
* type DatabaseError = TaggedError<"DatabaseError", { operation: string }, NetworkError | undefined>;
|
|
84
|
-
* const dbError: DatabaseError = {
|
|
85
|
-
* name: "DatabaseError",
|
|
86
|
-
* message: "Failed to connect to database",
|
|
87
|
-
* context: { operation: "connect" }, // Required!
|
|
88
|
-
* cause: networkError // Optional, but must be NetworkError if provided
|
|
89
|
-
* };
|
|
90
|
-
*
|
|
91
|
-
* // Discriminated unions still work
|
|
92
|
-
* function handleError(error: ValidationError | NetworkError) {
|
|
93
|
-
* switch (error.name) {
|
|
94
|
-
* case "ValidationError": // TypeScript knows this is ValidationError
|
|
95
|
-
* break;
|
|
96
|
-
* case "NetworkError": // TypeScript knows this is NetworkError
|
|
97
|
-
* break;
|
|
98
|
-
* }
|
|
99
|
-
* }
|
|
100
|
-
* ```
|
|
101
52
|
*/
|
|
102
|
-
type TaggedError<TName extends string = string, TContext extends
|
|
53
|
+
type TaggedError<TName extends string = string, TContext extends JsonObject | undefined = undefined, TCause extends AnyTaggedError | undefined = undefined> = Readonly<{
|
|
103
54
|
name: TName;
|
|
104
55
|
message: string;
|
|
105
56
|
} & WithContext<TContext> & WithCause<TCause>>;
|
|
@@ -108,222 +59,102 @@ type TaggedError<TName extends string = string, TContext extends Record<string,
|
|
|
108
59
|
/**
|
|
109
60
|
* Extracts a readable error message from an unknown error value
|
|
110
61
|
*
|
|
111
|
-
* This utility is commonly used in mapErr functions when converting
|
|
112
|
-
* unknown errors to typed error objects in the Result system.
|
|
113
|
-
*
|
|
114
62
|
* @param error - The unknown error to extract a message from
|
|
115
63
|
* @returns A string representation of the error
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* ```ts
|
|
119
|
-
* // With native Error
|
|
120
|
-
* const error = new Error("Something went wrong");
|
|
121
|
-
* const message = extractErrorMessage(error); // "Something went wrong"
|
|
122
|
-
*
|
|
123
|
-
* // With string error
|
|
124
|
-
* const stringError = "String error";
|
|
125
|
-
* const message2 = extractErrorMessage(stringError); // "String error"
|
|
126
|
-
*
|
|
127
|
-
* // With object error
|
|
128
|
-
* const unknownError = { code: 500, details: "Server error" };
|
|
129
|
-
* const message3 = extractErrorMessage(unknownError); // '{"code":500,"details":"Server error"}'
|
|
130
|
-
*
|
|
131
|
-
* // Used in mapErr function
|
|
132
|
-
* const result = await tryAsync({
|
|
133
|
-
* try: () => riskyOperation(),
|
|
134
|
-
* mapErr: (error) => Err({
|
|
135
|
-
* name: "NetworkError",
|
|
136
|
-
* message: extractErrorMessage(error),
|
|
137
|
-
* context: { operation: "riskyOperation" },
|
|
138
|
-
* cause: error,
|
|
139
|
-
* }),
|
|
140
|
-
* });
|
|
141
|
-
* ```
|
|
142
64
|
*/
|
|
143
65
|
declare function extractErrorMessage(error: unknown): string;
|
|
144
66
|
/**
|
|
145
67
|
* Replaces the "Error" suffix with "Err" suffix in error type names.
|
|
146
|
-
*
|
|
147
|
-
* @template T - An error type name that must end with "Error"
|
|
148
|
-
* @returns The type name with "Error" replaced by "Err"
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* ```ts
|
|
152
|
-
* type NetworkErr = ReplaceErrorWithErr<"NetworkError">; // "NetworkErr"
|
|
153
|
-
* type ValidationErr = ReplaceErrorWithErr<"ValidationError">; // "ValidationErr"
|
|
154
|
-
* ```
|
|
155
68
|
*/
|
|
156
69
|
type ReplaceErrorWithErr<T extends `${string}Error`> = T extends `${infer TBase}Error` ? `${TBase}Err` : never;
|
|
70
|
+
/**
|
|
71
|
+
* Input provided to the message template function.
|
|
72
|
+
* Contains everything the error will have except `message` (since that's what it computes).
|
|
73
|
+
*/
|
|
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
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Message template function type.
|
|
83
|
+
*/
|
|
84
|
+
type MessageFn<TName extends string, TContext extends JsonObject | undefined, TCause extends AnyTaggedError | undefined> = (input: MessageInput<TName, TContext, TCause>) => string;
|
|
157
85
|
/**
|
|
158
86
|
* Helper type that determines optionality based on whether T includes undefined.
|
|
159
|
-
* - If T includes undefined → property is optional
|
|
160
|
-
* - If T does not include undefined → property is required
|
|
161
87
|
*/
|
|
162
88
|
type OptionalIfUndefined<T, TKey extends string> = undefined extends T ? { [K in TKey]?: Exclude<T, undefined> } : { [K in TKey]: T };
|
|
163
89
|
/**
|
|
164
|
-
* Input type for error
|
|
165
|
-
*
|
|
166
|
-
*
|
|
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
|
|
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.
|
|
170
93
|
*/
|
|
171
|
-
type
|
|
172
|
-
message
|
|
173
|
-
} & (TContext extends undefined ?
|
|
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">);
|
|
174
97
|
/**
|
|
175
|
-
* The factories object returned by
|
|
98
|
+
* The final factories object returned by `.withMessage()`.
|
|
99
|
+
* Has factory functions, NO chain methods.
|
|
176
100
|
*/
|
|
177
|
-
type
|
|
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>> };
|
|
178
102
|
/**
|
|
179
103
|
* Builder interface for the fluent createTaggedError API.
|
|
180
|
-
*
|
|
104
|
+
* Has chain methods only, NO factory functions.
|
|
105
|
+
* Must call `.withMessage(fn)` to get factories.
|
|
181
106
|
*/
|
|
182
|
-
type ErrorBuilder<TName extends `${string}Error`, TContext extends
|
|
107
|
+
type ErrorBuilder<TName extends `${string}Error`, TContext extends JsonObject | undefined = undefined, TCause extends AnyTaggedError | undefined = undefined> = {
|
|
183
108
|
/**
|
|
184
109
|
* Constrains the context type for this error.
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* - `withContext<T>()` where T doesn't include undefined → context is **required**
|
|
188
|
-
* - `withContext<T | undefined>()` → context is **optional** but typed when provided
|
|
189
|
-
*
|
|
190
|
-
* @typeParam T - The shape of the context object. Include `| undefined` to make optional.
|
|
191
|
-
*
|
|
192
|
-
* @example Required context
|
|
193
|
-
* ```ts
|
|
194
|
-
* const { FileError } = createTaggedError('FileError')
|
|
195
|
-
* .withContext<{ path: string }>()
|
|
196
|
-
*
|
|
197
|
-
* FileError({ message: 'Not found', context: { path: '/etc/config' } }) // OK
|
|
198
|
-
* FileError({ message: 'Not found' }) // Type error: context required
|
|
199
|
-
* ```
|
|
200
|
-
*
|
|
201
|
-
* @example Optional but typed context
|
|
202
|
-
* ```ts
|
|
203
|
-
* const { LogError } = createTaggedError('LogError')
|
|
204
|
-
* .withContext<{ file: string; line: number } | undefined>()
|
|
205
|
-
*
|
|
206
|
-
* LogError({ message: 'Parse error' }) // OK
|
|
207
|
-
* LogError({ message: 'Parse error', context: { file: 'app.ts', line: 42 } }) // OK
|
|
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
|
-
* ```
|
|
110
|
+
* `.withContext<T>()` where T doesn't include undefined → context is **required**
|
|
111
|
+
* `.withContext<T | undefined>()` → context is **optional** but typed when provided
|
|
218
112
|
*/
|
|
219
|
-
withContext<T extends
|
|
113
|
+
withContext<T extends JsonObject | undefined = JsonObject | undefined>(): ErrorBuilder<TName, T, TCause>;
|
|
220
114
|
/**
|
|
221
115
|
* Constrains the cause type for this error.
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* - `withCause<T>()` where T doesn't include undefined → cause is **required**
|
|
225
|
-
* - `withCause<T | undefined>()` → cause is **optional** but typed when provided
|
|
226
|
-
*
|
|
227
|
-
* Since cause is typically optional, include `| undefined` in most cases.
|
|
228
|
-
*
|
|
229
|
-
* @typeParam T - The allowed cause type(s). Include `| undefined` to make optional.
|
|
230
|
-
*
|
|
231
|
-
* @example Optional typed cause (common)
|
|
232
|
-
* ```ts
|
|
233
|
-
* const { ServiceError } = createTaggedError('ServiceError')
|
|
234
|
-
* .withCause<DbError | CacheError | undefined>()
|
|
235
|
-
*
|
|
236
|
-
* ServiceError({ message: 'Failed' }) // OK
|
|
237
|
-
* ServiceError({ message: 'Failed', cause: dbError }) // OK
|
|
238
|
-
* ```
|
|
239
|
-
*
|
|
240
|
-
* @example Required cause (for wrapper errors)
|
|
241
|
-
* ```ts
|
|
242
|
-
* const { UnhandledError } = createTaggedError('UnhandledError')
|
|
243
|
-
* .withCause<AnyTaggedError>()
|
|
244
|
-
*
|
|
245
|
-
* UnhandledError({ message: 'Unexpected', cause: originalError }) // OK
|
|
246
|
-
* UnhandledError({ message: 'Unexpected' }) // Type error: cause required
|
|
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
|
-
* ```
|
|
116
|
+
* `.withCause<T>()` where T doesn't include undefined → cause is **required**
|
|
117
|
+
* `.withCause<T | undefined>()` → cause is **optional** but typed when provided
|
|
257
118
|
*/
|
|
258
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>;
|
|
259
127
|
};
|
|
260
128
|
/**
|
|
261
129
|
* Creates a new tagged error type with a fluent builder API.
|
|
262
130
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* - `{Name}Err`: Factory function that creates Err-wrapped TaggedError objects
|
|
266
|
-
* - `withContext<T>()`: Chain method to add context type
|
|
267
|
-
* - `withCause<T>()`: Chain method to add cause type
|
|
268
|
-
*
|
|
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.
|
|
131
|
+
* The builder provides `.withContext<T>()`, `.withCause<T>()`, and `.withMessage(fn)`.
|
|
132
|
+
* `.withMessage(fn)` is **required** and **terminal** — it returns the factory functions.
|
|
273
133
|
*
|
|
274
|
-
*
|
|
275
|
-
* Both `withContext` and `withCause` determine optionality based on whether
|
|
276
|
-
* the type includes `undefined`:
|
|
277
|
-
* - `T` without undefined → property is required
|
|
278
|
-
* - `T | undefined` → property is optional but typed when provided
|
|
279
|
-
*
|
|
280
|
-
* @template TName - The name of the error type (must end with "Error")
|
|
281
|
-
* @param name - The name of the error type
|
|
282
|
-
*
|
|
283
|
-
* @example Minimal error (no context, no cause)
|
|
134
|
+
* @example Simple error
|
|
284
135
|
* ```ts
|
|
285
|
-
* const {
|
|
286
|
-
*
|
|
287
|
-
* NetworkError({ message: 'Connection failed' })
|
|
288
|
-
* // Error only has { name: 'NetworkError', message: 'Connection failed' }
|
|
136
|
+
* const { RecorderBusyError, RecorderBusyErr } = createTaggedError('RecorderBusyError')
|
|
137
|
+
* .withMessage(() => 'A recording is already in progress');
|
|
289
138
|
* ```
|
|
290
139
|
*
|
|
291
|
-
* @example
|
|
140
|
+
* @example Error with context
|
|
292
141
|
* ```ts
|
|
293
|
-
* const {
|
|
294
|
-
* .withContext<{
|
|
295
|
-
*
|
|
296
|
-
* ApiError({ message: 'Failed', context: { endpoint: '/users', status: 500 } })
|
|
297
|
-
* // ApiError({ message: 'Failed' }) // Type error: context required
|
|
298
|
-
* ```
|
|
299
|
-
*
|
|
300
|
-
* @example Optional typed cause
|
|
301
|
-
* ```ts
|
|
302
|
-
* const { ServiceError } = createTaggedError('ServiceError')
|
|
303
|
-
* .withCause<DbError | CacheError | undefined>()
|
|
304
|
-
*
|
|
305
|
-
* ServiceError({ message: 'Failed' }) // OK
|
|
306
|
-
* ServiceError({ message: 'Failed', cause: dbError }) // OK, typed
|
|
307
|
-
* ```
|
|
308
|
-
*
|
|
309
|
-
* @example Full example with both
|
|
310
|
-
* ```ts
|
|
311
|
-
* const { UserServiceError } = createTaggedError('UserServiceError')
|
|
312
|
-
* .withContext<{ userId: string }>()
|
|
313
|
-
* .withCause<RepoError | undefined>()
|
|
314
|
-
*
|
|
315
|
-
* // Type extraction works
|
|
316
|
-
* type UserServiceError = ReturnType<typeof UserServiceError>
|
|
142
|
+
* const { DbNotFoundError, DbNotFoundErr } = createTaggedError('DbNotFoundError')
|
|
143
|
+
* .withContext<{ table: string; id: string }>()
|
|
144
|
+
* .withMessage(({ context }) => `${context.table} '${context.id}' not found`);
|
|
317
145
|
* ```
|
|
318
146
|
*
|
|
319
|
-
* @example
|
|
147
|
+
* @example Error with context and cause
|
|
320
148
|
* ```ts
|
|
321
|
-
* const {
|
|
322
|
-
* .withContext<
|
|
323
|
-
* .withCause<
|
|
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
|
+
* );
|
|
324
155
|
* ```
|
|
325
156
|
*/
|
|
326
157
|
declare function createTaggedError<TName extends `${string}Error`>(name: TName): ErrorBuilder<TName>;
|
|
327
158
|
//#endregion
|
|
328
|
-
export { AnyTaggedError, TaggedError, createTaggedError, extractErrorMessage };
|
|
159
|
+
export { AnyTaggedError, JsonObject, JsonValue, TaggedError, createTaggedError, extractErrorMessage };
|
|
329
160
|
//# 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":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/error/types.ts","../../src/error/utils.ts"],"sourcesContent":[],"mappings":"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"}
|