wellcrafted 0.17.0 β†’ 0.19.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
@@ -7,125 +7,60 @@
7
7
 
8
8
  *Delightful TypeScript utilities for elegant, type-safe applications*
9
9
 
10
- This library provides a robust, Rust-inspired `Result` type, elegant brand types, and a lightweight, serializable error handling system for TypeScript. It's designed to help you write more predictable, type-safe, and composable code by making error handling an explicit part of your function signatures.
10
+ ## Transform unpredictable errors into type-safe results
11
11
 
12
- ## Table of Contents
13
-
14
- - [Quick Start](#quick-start)
15
- - [Core Idea: The Result Type](#core-idea-the-result-type)
16
- - [Installation](#installation)
17
- - [Handling Operation Outcomes](#handling-operation-outcomes)
18
- - [Understanding TaggedError](#understanding-taggederror)
19
- - [Basic Usage](#basic-usage)
20
- - [Wrapping Functions That Throw](#wrapping-functions-that-throw)
21
- - [API Reference](#api-reference)
22
- - [Design Philosophy](#design-philosophy)
23
- - [FAQ](#faq)
24
-
25
- ## Quick Start
26
-
27
- **30-second example:** Transform throwing async operations into type-safe Results.
28
-
29
- ```bash
30
- npm install wellcrafted
31
- ```
32
-
33
- ```ts
34
- import { tryAsync } from "wellcrafted/result";
35
- import { type TaggedError } from "wellcrafted/error";
36
- import * as fs from 'fs/promises';
37
-
38
- type FileError = TaggedError<"FileError">;
39
-
40
- async function readConfig(path: string) {
41
- return tryAsync<string, FileError>({
42
- try: async () => {
43
- const content = await fs.readFile(path, 'utf-8');
44
- return content;
45
- },
46
- mapError: (error) => ({
47
- name: "FileError",
48
- message: "Failed to read configuration file",
49
- context: { path },
50
- cause: error
51
- })
52
- });
12
+ ```typescript
13
+ // ❌ Before: Which errors can this throw? 🀷
14
+ try {
15
+ await saveUser(user);
16
+ } catch (error) {
17
+ // ... good luck debugging in production
53
18
  }
54
19
 
55
- // Handle the result
56
- const { data, error } = await readConfig('./config.json');
57
-
20
+ // βœ… After: Every error is visible and typed
21
+ const { data, error } = await saveUser(user);
58
22
  if (error) {
59
- console.error(`${error.name}: ${error.message}`);
60
- console.log("Context:", error.context);
61
- process.exit(1);
23
+ switch (error.name) {
24
+ case "ValidationError":
25
+ showToast(`Invalid ${error.context.field}`);
26
+ break;
27
+ case "AuthError":
28
+ redirectToLogin();
29
+ break;
30
+ // TypeScript ensures you handle all cases!
31
+ }
62
32
  }
63
-
64
- console.log("Config loaded:", data); // TypeScript knows data is safe here
65
33
  ```
66
34
 
67
- **What just happened?** Instead of letting file operations throw unpredictable errors, `tryAsync` wraps them in a `Result` type that makes success and failure explicit in your function signatures. No more unhandled exceptions!
68
-
69
- ---
70
-
71
- ## Core Idea: The Result Type
72
-
73
- > **πŸ’‘ TL;DR:** Replace `throw new Error()` with `return Err()` to make errors visible in your function signatures.
74
-
75
- JavaScript's traditional error handling, based on `try...catch` and throwing `Error` objects, has two major drawbacks for modern application development:
76
- 1. **It's not type-safe**: A function signature `function doSomething(): User` doesn't tell you that it might throw a `NetworkError` or a `ValidationError`. Errors are invisible until they strike at runtime.
77
- 2. **It's not serialization-friendly**: `Error` instances lose their prototype chain when crossing serialization boundaries (JSON.stringify/parse, network requests, worker threads), breaking `instanceof` checks.
78
-
79
- This library solves these problems with the `Result<T, E>` type. Instead of throwing, functions return a `Result` object that explicitly represents either a success or a failure.
80
-
81
- A `Result` is a union of two "variants":
82
- - **`Ok<T>`**: Represents a successful outcome, containing a `data` field with the success value. In this variant, the `error` property is always `null`.
83
- - **`Err<E>`**: Represents a failure outcome, containing an `error` field with the error value. In this variant, the `data` property is always `null`.
84
-
85
- This structure allows TypeScript's control-flow analysis to act as if it's a **discriminated union**. By checking if `result.error === null`, TypeScript knows it must be an `Ok` variant and can safely access `result.data`. This makes error handling explicit, type-safe, and predictable.
35
+ ## A collection of simple, powerful primitives
86
36
 
87
- ### Anatomy of a Result Type
88
-
89
- Here's the complete TypeScript implementation - it's simpler than you might think:
90
-
91
- ```ts
92
- // The two possible outcomes
93
- export type Ok<T> = { data: T; error: null };
94
- export type Err<E> = { error: E; data: null };
95
-
96
- // Result is just a union of these two types
97
- export type Result<T, E> = Ok<T> | Err<E>;
98
-
99
- // Helper functions to create each variant
100
- export const Ok = <T>(data: T): Ok<T> => ({ data, error: null });
101
- export const Err = <E>(error: E): Err<E> => ({ error, data: null });
37
+ ### 🎯 Result Type
38
+ Make errors explicit in function signatures
39
+ ```typescript
40
+ function divide(a: number, b: number): Result<number, string> {
41
+ if (b === 0) return Err("Division by zero");
42
+ return Ok(a / b);
43
+ }
102
44
  ```
103
45
 
104
- **That's it!** The entire foundation is built on this elegant simplicity:
105
-
106
- - **`Ok<T>`** always has `data: T` and `error: null`
107
- - **`Err<E>`** always has `error: E` and `data: null`
108
- - **`Result<T, E>`** is simply `Ok<T> | Err<E>`
109
-
110
- This design creates a **discriminated union** where the `error` (or `data`) property acts as the discriminant (with literal types `null` vs non-null), allowing TypeScript to automatically narrow types:
46
+ ### 🏷️ Brand Types
47
+ Create distinct types from primitives
48
+ ```typescript
49
+ type UserId = Brand<string, "UserId">;
50
+ type OrderId = Brand<string, "OrderId">;
111
51
 
112
- ```ts
113
- function handleResult<T, E>(result: Result<T, E>) {
114
- if (result.error === null) {
115
- // TypeScript knows this is Ok<T>
116
- console.log(result.data); // βœ… data is type T
117
- // console.log(result.error); // ❌ TypeScript knows this is null
118
- } else {
119
- // TypeScript knows this is Err<E>
120
- console.log(result.error); // βœ… error is type E
121
- // console.log(result.data); // ❌ TypeScript knows this is null
122
- }
123
- }
52
+ // TypeScript prevents mixing them up!
53
+ function getUser(id: UserId) { /* ... */ }
124
54
  ```
125
55
 
126
- The beauty is in the transparency - you can see exactly how it works under the hood, yet it provides powerful type safety and ergonomics.
56
+ ### πŸ“‹ Tagged Errors
57
+ Structured, serializable errors with convenient factory functions
58
+ ```typescript
59
+ import { createTaggedError } from "wellcrafted/error";
127
60
 
128
- ---
61
+ const { ApiError, ApiErr } = createTaggedError("ApiError");
62
+ // ApiError() creates error object, ApiErr() creates Err-wrapped error
63
+ ```
129
64
 
130
65
  ## Installation
131
66
 
@@ -133,731 +68,423 @@ The beauty is in the transparency - you can see exactly how it works under the h
133
68
  npm install wellcrafted
134
69
  ```
135
70
 
136
- ---
137
-
138
- ## Handling Operation Outcomes
139
-
140
- Once you have a `Result`, there are two main patterns for working with it. Choose the pattern that best fits your preference for code style and the specific context of your code.
141
-
142
- ### Pattern 1: Destructuring (Preferred)
143
-
144
- This pattern will feel familiar to developers working with modern libraries like Supabase or Astro Actions. You can destructure the `data` and `error` properties directly from the result object and use a simple conditional check on the `error` property.
145
-
146
- This approach is often cleaner and more direct for handling the two possible outcomes, as it gives you immediate access to the inner `data` and `error` values.
71
+ ## Quick Start
147
72
 
148
- ```ts
149
- const { data, error } = someOperation();
73
+ ```typescript
74
+ import { tryAsync } from "wellcrafted/result";
75
+ import { createTaggedError } from "wellcrafted/error";
76
+
77
+ // Define your error with factory function
78
+ const { ApiError, ApiErr } = createTaggedError("ApiError");
79
+ type ApiError = ReturnType<typeof ApiError>;
80
+
81
+ // Wrap any throwing operation
82
+ const { data, error } = await tryAsync({
83
+ try: () => fetch('/api/user').then(r => r.json()),
84
+ mapError: (error) => ApiError({
85
+ message: "Failed to fetch user",
86
+ context: { endpoint: '/api/user' },
87
+ cause: error
88
+ })
89
+ });
150
90
 
151
91
  if (error) {
152
- // `error` holds the inner error value from the Err variant.
153
- console.error(`An error occurred: ${error.message}`);
154
- return; // Or handle the error appropriately
155
- }
156
-
157
- // If `error` is null, `data` holds the inner success value from the Ok variant.
158
- // In most modern TypeScript setups, `data` will be correctly inferred as the success type.
159
- console.log(`The result is: ${data}`);
160
- ```
161
-
162
- ### Pattern 2: Using Type Guards
163
-
164
- In some complex scenarios or with certain TypeScript configurations, the compiler might not be able to perfectly infer the relationship between `data` and `error` when they are destructured into separate variables. In these cases, using the `isOk()` and `isErr()` type guards is a more robust solution. TypeScript's control flow analysis is designed to work flawlessly with this pattern, guaranteeing type safety within each conditional block.
165
-
166
- ```ts
167
- import { isOk, isErr } from "wellcrafted/result";
168
-
169
- const result = someOperation();
170
-
171
- if (isErr(result)) {
172
- // TypeScript *guarantees* that `result` is `Err<E>` here.
173
- // The `result.data` property is `null`.
174
- // The `result.error` property contains the error value.
175
- const errorValue = result.error;
176
- console.error(errorValue);
177
-
92
+ console.error(`${error.name}: ${error.message}`);
178
93
  } else {
179
- // If it's not an error, it must be a success.
180
- // TypeScript *guarantees* that `result` is `Ok<T>` here.
181
- // The `result.error` property is `null`.
182
- // The `result.data` property contains the success value.
183
- const successValue = result.data;
184
- console.log(successValue);
94
+ console.log("User:", data);
185
95
  }
186
96
  ```
187
97
 
188
- > **When to use Type Guards:** While destructuring is preferred for its simplicity, reach for `isOk()` and `isErr()` whenever you notice that TypeScript isn't correctly narrowing the type of `data` after an error check. This ensures your code remains fully type-safe without needing manual type assertions.
98
+ ## Core Features
189
99
 
190
- ---
100
+ <table>
101
+ <tr>
102
+ <td>
191
103
 
192
- ## Understanding TaggedError
104
+ **🎯 Explicit Error Handling**
105
+ All errors visible in function signatures
193
106
 
194
- This library promotes a **serializable, type-safe error system** using plain objects instead of JavaScript's `Error` class. The foundation of this system is the `TaggedError` type.
107
+ </td>
108
+ <td>
195
109
 
196
- ### Why Plain Objects for Errors?
110
+ **πŸ“¦ Serialization-Safe**
111
+ Plain objects work everywhere
197
112
 
198
- 1. **Serialization-First**: Plain objects can be easily serialized to JSON (`JSON.stringify`) and transmitted across boundaries (network APIs, IPC, web workers) without losing information, unlike `Error` classes.
199
- 2. **Type Safety**: Use TypeScript's literal and union types to create a discriminated union of possible errors, allowing `switch` statements to safely narrow down error types.
200
- 3. **Lightweight**: Avoids the overhead of class instantiation and the complexities of `instanceof` checks.
201
- 4. **Structured Context**: Easily enforce that all errors carry structured, machine-readable context.
113
+ </td>
114
+ <td>
202
115
 
203
- Every `TaggedError` contains four essential properties that work together to create a robust, debuggable error system:
116
+ **✨ Elegant API**
117
+ Clean, intuitive patterns
204
118
 
205
- ### The Four Properties
119
+ </td>
120
+ </tr>
121
+ <tr>
122
+ <td>
206
123
 
207
- ```ts
208
- type TaggedError<T extends string> = {
209
- readonly name: T; // 1. The discriminant
210
- message: string; // 2. Human-readable description
211
- context: Record<string, unknown>; // 3. Debugging data
212
- cause?: unknown; // 4. Root cause (optional)
213
- };
214
- ```
124
+ **πŸ” Zero Magic**
125
+ ~50 lines of core code
215
126
 
216
- #### 1. **`name`** - The Discriminant (Tagged Field)
127
+ </td>
128
+ <td>
217
129
 
218
- This is your error's unique identifier and the key to pattern matching. Use it in `if` statements and `switch` statements to handle different error types:
219
-
220
- ```ts
221
- type ValidationError = TaggedError<"ValidationError">;
222
- type NetworkError = TaggedError<"NetworkError">;
223
- type FileError = TaggedError<"FileError">;
224
-
225
- function handleError(error: ValidationError | NetworkError | FileError) {
226
- switch (error.name) {
227
- case "ValidationError":
228
- // TypeScript knows this is ValidationError
229
- console.log("Invalid input:", error.context);
230
- break;
231
- case "NetworkError":
232
- // TypeScript knows this is NetworkError
233
- console.log("Network failed:", error.message);
234
- break;
235
- case "FileError":
236
- // TypeScript knows this is FileError
237
- console.log("File issue:", error.context);
238
- break;
239
- }
240
- }
241
- ```
130
+ **πŸš€ Lightweight**
131
+ Zero dependencies, < 2KB
242
132
 
243
- #### 2. **`message`** - Human-Readable Text
133
+ </td>
134
+ <td>
244
135
 
245
- Pure text description that explains what went wrong. Keep it clear and actionable:
136
+ **🎨 Composable**
137
+ Mix and match utilities
246
138
 
247
- ```ts
248
- return Err({
249
- name: "ValidationError",
250
- message: "Email address must contain an @ symbol", // Clear, specific
251
- context: { email: userInput },
252
- cause: undefined
253
- });
254
- ```
139
+ </td>
140
+ </tr>
141
+ </table>
255
142
 
256
- #### 3. **`context`** - Debugging Data
143
+ ## The Result Pattern Explained
257
144
 
258
- Include function inputs and any data that would help debug the issue. This is invaluable for logging and troubleshooting:
145
+ The Result type makes error handling explicit and type-safe:
259
146
 
260
- ```ts
261
- function processUser(id: number, options: UserOptions): Result<User, ProcessError> {
262
- return Err({
263
- name: "ProcessError",
264
- message: "User processing failed",
265
- context: {
266
- userId: id, // Function input
267
- options, // Function input
268
- timestamp: new Date().toISOString(), // Additional context
269
- retryCount: 3 // Useful debugging info
270
- },
271
- cause: undefined
272
- });
273
- }
147
+ ```typescript
148
+ // The entire implementation
149
+ type Ok<T> = { data: T; error: null };
150
+ type Err<E> = { error: E; data: null };
151
+ type Result<T, E> = Ok<T> | Err<E>;
274
152
  ```
275
153
 
276
- #### 4. **`cause`** - Root Cause Bubbling
277
-
278
- - **For new errors**: Set `cause: undefined`
279
- - **For wrapping existing errors**: Pass the original error as `cause`
154
+ **The Magic**: This creates a discriminated union where TypeScript automatically narrows types:
280
155
 
281
- ```ts
282
- // Creating a new error
283
- return Err({
284
- name: "ValidationError",
285
- message: "Invalid user data",
286
- context: { input },
287
- cause: undefined // New error, no underlying cause
288
- });
289
-
290
- // Wrapping an existing error
291
- try {
292
- await database.save(user);
293
- } catch (dbError) {
294
- return Err({
295
- name: "SaveError",
296
- message: "Failed to save user",
297
- context: { userId: user.id },
298
- cause: dbError // Bubble up the original database error
299
- });
156
+ ```typescript
157
+ if (result.error) {
158
+ // TypeScript knows: error is E, data is null
159
+ } else {
160
+ // TypeScript knows: data is T, error is null
300
161
  }
301
162
  ```
302
163
 
303
- ### Creating Domain-Specific Errors
164
+ ## Basic Patterns
304
165
 
305
- You can define a set of possible errors for a specific domain:
166
+ ### Handle Results with Destructuring
306
167
 
307
168
  ```typescript
308
- // Define your specific error types
309
- export type FileNotFoundError = TaggedError<"FileNotFoundError">;
310
- export type PermissionDeniedError = TaggedError<"PermissionDeniedError">;
311
- export type DiskFullError = TaggedError<"DiskFullError">;
312
-
313
- // Create a union of all possible errors for this domain
314
- export type FileSystemError = FileNotFoundError | PermissionDeniedError | DiskFullError;
169
+ const { data, error } = await someOperation();
315
170
 
316
- // A factory function to create an error
317
- function createFileNotFoundError(path: string, cause?: unknown): FileNotFoundError {
318
- return {
319
- name: "FileNotFoundError",
320
- message: `The file at path "${path}" was not found.`,
321
- context: { path },
322
- cause
323
- };
171
+ if (error) {
172
+ // Handle error with full type safety
173
+ return;
324
174
  }
325
- ```
326
175
 
327
- Because `name` is a unique literal type for each error, TypeScript can use it to discriminate between them in a `switch` statement:
328
-
329
- ```ts
330
- function handleError(error: FileSystemError) {
331
- switch (error.name) {
332
- case "FileNotFoundError":
333
- // TypeScript knows `error` is `FileNotFoundError` here.
334
- console.error(`Path not found: ${error.context.path}`);
335
- break;
336
- case "PermissionDeniedError":
337
- // TypeScript knows `error` is `PermissionDeniedError` here.
338
- console.error("Permission was denied.");
339
- break;
340
- case "DiskFullError":
341
- // ...
342
- break;
343
- }
344
- }
176
+ // Use data - TypeScript knows it's safe
345
177
  ```
346
178
 
347
- ### Best Practices for Errors
348
-
349
- #### 1. Include Meaningful Context
350
- Always include function inputs and other relevant state in the `context` object. This is invaluable for logging and debugging.
179
+ ### Wrap Unsafe Operations
351
180
 
352
181
  ```typescript
353
- function createDbError(
354
- message: string,
355
- query: string,
356
- params: unknown[],
357
- cause: unknown
358
- ): DbError {
359
- return {
360
- name: "DbError",
361
- message,
362
- context: {
363
- query,
364
- params,
365
- timestamp: new Date().toISOString(),
366
- },
367
- cause,
368
- };
369
- }
370
- ```
371
-
372
- #### 2. Handle Errors at the Right Level
373
- Handle or transform errors where you can add more context or make a recovery decision.
374
-
375
- ```ts
376
- async function initializeApp(): Promise<Result<App, FsError | ValidationError>> {
377
- const configResult = await readConfig("./config.json");
378
-
379
- // Propagate the file system error directly if config read fails
380
- if (isErr(configResult)) {
381
- return configResult;
382
- }
383
-
384
- // If config is read, but is invalid, return a *different* kind of error
385
- const validationResult = validateConfig(configResult.data);
386
- if (isErr(validationResult)) {
387
- return validationResult;
388
- }
182
+ // Synchronous
183
+ const result = trySync({
184
+ try: () => JSON.parse(jsonString),
185
+ mapError: (error) => ({
186
+ name: "ParseError",
187
+ message: "Invalid JSON",
188
+ context: { input: jsonString },
189
+ cause: error
190
+ })
191
+ });
389
192
 
390
- return Ok(new App(validationResult.data));
391
- }
193
+ // Asynchronous
194
+ const result = await tryAsync({
195
+ try: () => fetch(url),
196
+ mapError: (error) => ({
197
+ name: "NetworkError",
198
+ message: "Request failed",
199
+ context: { url },
200
+ cause: error
201
+ })
202
+ });
392
203
  ```
393
204
 
394
- This structure makes errors **trackable**, **debuggable**, and **type-safe** while maintaining clean separation between different failure modes in your application.
395
-
396
- ---
397
-
398
- ## Basic Usage
205
+ ### Service Layer Example
399
206
 
400
- Now that you understand how to handle Result values and the TaggedError structure, let's see complete examples that combine both concepts:
401
-
402
- ```ts
403
- import { Result, Ok, Err, isOk } from "wellcrafted/result";
404
- import { type TaggedError } from "wellcrafted/error";
405
-
406
- // --- Example 1: A Safe Division Function ---
207
+ ```typescript
208
+ import { Result, Ok, tryAsync } from "wellcrafted/result";
209
+ import { createTaggedError } from "wellcrafted/error";
407
210
 
408
- // 1. Define a specific error for math-related failures
409
- type MathError = TaggedError<"MathError">;
211
+ // Define service-specific errors
212
+ const { ValidationError, ValidationErr } = createTaggedError("ValidationError");
213
+ const { DatabaseError, DatabaseErr } = createTaggedError("DatabaseError");
410
214
 
411
- // 2. Create a function that returns a Result with our structured error
412
- function divide(numerator: number, denominator: number): Result<number, MathError> {
413
- if (denominator === 0) {
414
- return Err({
415
- name: "MathError",
416
- message: "Cannot divide by zero.",
417
- context: { numerator, denominator },
418
- cause: undefined
419
- });
420
- }
421
- return Ok(numerator / denominator);
422
- }
215
+ type ValidationError = ReturnType<typeof ValidationError>;
216
+ type DatabaseError = ReturnType<typeof DatabaseError>;
423
217
 
424
- // 3. Handle the result
425
- const divisionResult = divide(10, 0);
218
+ // Factory function pattern - no classes!
219
+ export function createUserService(db: Database) {
220
+ return {
221
+ async createUser(input: CreateUserInput): Promise<Result<User, ValidationError | DatabaseError>> {
222
+ // Direct return with Err variant
223
+ if (!input.email.includes('@')) {
224
+ return ValidationErr({
225
+ message: "Invalid email format",
226
+ context: { field: 'email', value: input.email },
227
+ cause: undefined
228
+ });
229
+ }
426
230
 
427
- if (!isOk(divisionResult)) {
428
- // `divisionResult.error` is a fully-typed MathError object
429
- console.error(`Error (${divisionResult.error.name}): ${divisionResult.error.message}`);
430
- console.log("Context:", divisionResult.error.context); // { numerator: 10, denominator: 0 }
431
- }
231
+ return tryAsync({
232
+ try: () => db.save(input),
233
+ mapError: (error) => DatabaseError({
234
+ message: "Failed to save user",
235
+ context: { operation: 'createUser', input },
236
+ cause: error
237
+ })
238
+ });
239
+ },
432
240
 
433
- // --- Example 2: Parsing a User Object ---
434
-
435
- // 1. Define a specific error for parsing failures
436
- type ParseError = TaggedError<"ParseError">;
437
-
438
- // 2. Create a function that returns a Result with our structured error
439
- function parseUser(json: string): Result<{ name: string }, ParseError> {
440
- try {
441
- const data = JSON.parse(json);
442
- if (typeof data.name !== "string") {
443
- return Err({
444
- name: "ParseError",
445
- message: "User object must have a name property of type string.",
446
- context: { receivedValue: data.name },
447
- cause: undefined
241
+ async getUser(id: string): Promise<Result<User | null, DatabaseError>> {
242
+ return tryAsync({
243
+ try: () => db.findById(id),
244
+ mapError: (error) => DatabaseError({
245
+ message: "Failed to fetch user",
246
+ context: { userId: id },
247
+ cause: error
248
+ })
448
249
  });
449
250
  }
450
- return Ok(data);
451
- } catch (e) {
452
- return Err({
453
- name: "ParseError",
454
- message: "Invalid JSON provided.",
455
- context: { rawString: json },
456
- cause: e,
457
- });
458
- }
251
+ };
459
252
  }
460
253
 
461
- // 3. Handle the result
462
- const userResult = parseUser('{"name": "Alice"}');
254
+ // Export type for the service
255
+ export type UserService = ReturnType<typeof createUserService>;
463
256
 
464
- if (isOk(userResult)) {
465
- console.log(`Welcome, ${userResult.data.name}!`);
466
- } else {
467
- // `userResult.error` is a fully-typed ParseError object
468
- console.error(`Error (${userResult.error.name}): ${userResult.error.message}`);
469
- console.log("Context:", userResult.error.context);
470
- }
257
+ // Create a live instance (dependency injection at build time)
258
+ export const UserServiceLive = createUserService(databaseInstance);
471
259
  ```
472
260
 
473
- ---
474
-
475
- ## Wrapping Functions That Throw
261
+ ## Why wellcrafted?
476
262
 
477
- When integrating with existing code that throws exceptions (like `JSON.parse`, fetch APIs, or database clients), you'll need a way to convert these throwing functions into safe `Result`-returning functions. This library provides `trySync` and `tryAsync` to handle this conversion seamlessly.
263
+ JavaScript's `try-catch` has fundamental problems:
478
264
 
479
- ### Synchronous Operations with `trySync`
265
+ 1. **Invisible Errors**: Function signatures don't show what errors can occur
266
+ 2. **Lost in Transit**: `JSON.stringify(new Error())` loses critical information
267
+ 3. **No Type Safety**: TypeScript can't help with `catch (error)` blocks
268
+ 4. **Inconsistent**: Libraries throw different things (strings, errors, objects, undefined)
480
269
 
481
- Use `trySync` for synchronous functions that might throw. You provide the operation and a `mapError` function to transform the caught exception into your desired error type.
270
+ wellcrafted solves these with simple, composable primitives that make errors:
271
+ - **Explicit** in function signatures
272
+ - **Serializable** across all boundaries
273
+ - **Type-safe** with full TypeScript support
274
+ - **Consistent** with structured error objects
482
275
 
483
- ```ts
484
- import { trySync, Result } from "wellcrafted/result";
485
- import { type TaggedError } from "wellcrafted/error";
276
+ ## Service Pattern Best Practices
486
277
 
487
- type ParseError = TaggedError<"ParseError">;
278
+ Based on real-world usage, here's the recommended pattern for creating services with wellcrafted:
488
279
 
489
- function parseJson(raw: string): Result<object, ParseError> {
490
- return trySync({
491
- try: () => JSON.parse(raw),
492
- mapError: (error) => ({
493
- name: "ParseError",
494
- message: "Failed to parse JSON",
495
- context: { raw },
496
- cause: error
497
- })
498
- });
499
- }
280
+ ### Factory Function Pattern
500
281
 
501
- const result = parseJson('{"key": "value"}'); // Ok<{key: string}>
502
- const failedResult = parseJson('not json'); // Err<ParseError>
503
- ```
504
-
505
- ### Asynchronous Operations with `tryAsync`
506
-
507
- Use `tryAsync` for functions that return a `Promise`. It handles both rejected promises and synchronous throws within the async function.
508
-
509
- ```ts
510
- import { tryAsync, Result } from "wellcrafted/result";
511
- import { type TaggedError } from "wellcrafted/error";
512
-
513
- type User = { id: number; name: string };
514
- type NetworkError = TaggedError<"NetworkError">;
515
-
516
- async function fetchUser(userId: number): Promise<Result<User, NetworkError>> {
517
- return tryAsync({
518
- try: async () => {
519
- const response = await fetch(`https://api.example.com/users/${userId}`);
520
- if (!response.ok) {
521
- // You can throw a custom error object
522
- throw { message: "Request failed", statusCode: response.status };
282
+ ```typescript
283
+ import { createTaggedError } from "wellcrafted/error";
284
+
285
+ // 1. Define service-specific errors
286
+ const { RecorderServiceError, RecorderServiceErr } = createTaggedError("RecorderServiceError");
287
+ type RecorderServiceError = ReturnType<typeof RecorderServiceError>;
288
+
289
+ // 2. Create service with factory function
290
+ export function createRecorderService() {
291
+ // Private state in closure
292
+ let isRecording = false;
293
+
294
+ // Return object with methods
295
+ return {
296
+ startRecording(): Result<void, RecorderServiceError> {
297
+ if (isRecording) {
298
+ return RecorderServiceErr({
299
+ message: "Already recording",
300
+ context: { isRecording },
301
+ cause: undefined
302
+ });
523
303
  }
524
- return response.json();
304
+
305
+ isRecording = true;
306
+ return Ok(undefined);
525
307
  },
526
- mapError: (error) => ({
527
- name: "NetworkError",
528
- message: "Failed to fetch user",
529
- context: { userId },
530
- cause: error
531
- })
532
- });
308
+
309
+ stopRecording(): Result<Blob, RecorderServiceError> {
310
+ if (!isRecording) {
311
+ return RecorderServiceErr({
312
+ message: "Not currently recording",
313
+ context: { isRecording },
314
+ cause: undefined
315
+ });
316
+ }
317
+
318
+ isRecording = false;
319
+ return Ok(new Blob(["audio data"]));
320
+ }
321
+ };
533
322
  }
534
323
 
535
- const userResult = await fetchUser(1);
536
- ```
324
+ // 3. Export type
325
+ export type RecorderService = ReturnType<typeof createRecorderService>;
537
326
 
538
- ### Type Safety with Generics
327
+ // 4. Create singleton instance
328
+ export const RecorderServiceLive = createRecorderService();
329
+ ```
539
330
 
540
- When using `trySync` and `tryAsync`, you have two approaches to ensure your `mapError` function returns the correct TaggedError type:
331
+ ### Platform-Specific Services
541
332
 
542
- #### Approach 1: Explicit Generics
333
+ For services that need different implementations per platform:
543
334
 
544
- Pass the success type and error type as generic parameters. This approach is clear and explicit about the expected types:
335
+ ```typescript
336
+ // types.ts - shared interface
337
+ export type FileService = {
338
+ readFile(path: string): Promise<Result<string, FileServiceError>>;
339
+ writeFile(path: string, content: string): Promise<Result<void, FileServiceError>>;
340
+ };
545
341
 
546
- ```ts
547
- // For tryAsync
548
- async function readConfig(path: string) {
549
- return tryAsync<string, FileError>({ // πŸ‘ˆ <SuccessType, ErrorType>
550
- try: async () => {
551
- const content = await fs.readFile(path, 'utf-8');
552
- return content;
342
+ // desktop.ts
343
+ export function createFileServiceDesktop(): FileService {
344
+ return {
345
+ async readFile(path) {
346
+ // Desktop implementation using Node.js APIs
553
347
  },
554
- mapError: (error) => ({
555
- name: "FileError",
556
- message: "Failed to read configuration file",
557
- context: { path },
558
- cause: error
559
- })
560
- });
348
+ async writeFile(path, content) {
349
+ // Desktop implementation
350
+ }
351
+ };
561
352
  }
562
353
 
563
- // For trySync
564
- function parseConfig(content: string) {
565
- return trySync<Config, ParseError>({ // πŸ‘ˆ <SuccessType, ErrorType>
566
- try: () => {
567
- const parsed = JSON.parse(content);
568
- validateConfig(parsed); // throws if invalid
569
- return parsed as Config;
354
+ // web.ts
355
+ export function createFileServiceWeb(): FileService {
356
+ return {
357
+ async readFile(path) {
358
+ // Web implementation using File API
570
359
  },
571
- mapError: (error) => ({
572
- name: "ParseError",
573
- message: "Invalid configuration format",
574
- context: { contentLength: content.length },
575
- cause: error
576
- })
577
- });
360
+ async writeFile(path, content) {
361
+ // Web implementation
362
+ }
363
+ };
578
364
  }
579
- ```
580
365
 
581
- #### Approach 2: Return Type Annotation on mapError
366
+ // index.ts - runtime selection
367
+ export const FileServiceLive = typeof window !== 'undefined'
368
+ ? createFileServiceWeb()
369
+ : createFileServiceDesktop();
370
+ ```
582
371
 
583
- Annotate the return type of the `mapError` function. This approach can be cleaner when generics would format awkwardly:
372
+ ## Common Use Cases
584
373
 
585
- ```ts
586
- // For tryAsync
587
- async function saveUser(user: User) {
588
- return tryAsync({
589
- try: async () => {
590
- const result = await db.users.insert(user);
591
- return result.id;
592
- },
593
- mapError: (error): DatabaseError => ({ // πŸ‘ˆ Annotate return type
594
- name: "DatabaseError",
595
- message: "Failed to save user to database",
596
- context: { userId: user.id },
597
- cause: error
598
- })
599
- });
600
- }
374
+ <details>
375
+ <summary><b>API Route Handler</b></summary>
601
376
 
602
- // For trySync
603
- function validateEmail(email: string) {
604
- return trySync({
605
- try: () => {
606
- if (!email.includes('@')) {
607
- throw new Error('Invalid email format');
608
- }
609
- return email.toLowerCase();
610
- },
611
- mapError: (error): ValidationError => ({ // πŸ‘ˆ Annotate return type
612
- name: "ValidationError",
613
- message: "Email validation failed",
614
- context: { email },
615
- cause: error
616
- })
617
- });
377
+ ```typescript
378
+ export async function GET(request: Request) {
379
+ const result = await userService.getUser(params.id);
380
+
381
+ if (result.error) {
382
+ switch (result.error.name) {
383
+ case "UserNotFoundError":
384
+ return new Response("Not found", { status: 404 });
385
+ case "DatabaseError":
386
+ return new Response("Server error", { status: 500 });
387
+ }
388
+ }
389
+
390
+ return Response.json(result.data);
618
391
  }
619
392
  ```
393
+ </details>
620
394
 
621
- **Key points:**
622
- - Both approaches ensure `mapError` returns your exact TaggedError type
623
- - Avoid using `as const` - always map to proper TaggedError objects
624
- - Choose explicit generics for clarity, or return type annotation for brevity
625
- - The important thing is ensuring type safety for your error handling
626
-
627
- ---
628
-
629
- ## Partitioning Results
630
-
631
- When working with multiple asynchronous operations that return `Result` objects, you'll often need to separate the successful results from the failed ones. The `partitionResults` utility function makes this easy by splitting an array of Results into two separate arrays.
632
-
633
- ### When to Use `partitionResults`
395
+ <details>
396
+ <summary><b>Form Validation</b></summary>
634
397
 
635
- Use `partitionResults` when you have:
636
- - Multiple async operations that might fail independently
637
- - A need to handle all errors collectively
638
- - Successful results that should be processed together
639
-
640
- ### Common Pattern: Map β†’ Filter β†’ Partition
641
-
642
- This is a typical workflow when processing multiple commands or operations:
643
-
644
- ```ts
645
- import { partitionResults } from "wellcrafted/result";
646
-
647
- // Example: Processing multiple commands that might fail
648
- const results = await Promise.all(
649
- commands
650
- .map((command) => {
651
- const config = getCommandConfig(command.id);
652
- if (!config) return; // Early return if config missing
653
- return executeCommand({ command, config });
654
- })
655
- .filter((result) => result !== undefined) // Remove undefined values
656
- );
657
-
658
- const { oks, errs } = partitionResults(results);
659
-
660
- // Handle all errors at once
661
- if (errs.length > 0) {
662
- const errorMessages = errs.map(({ error }) => error.message).join(', ');
663
- showNotification(`${errs.length} operations failed: ${errorMessages}`);
664
- return;
398
+ ```typescript
399
+ function validateLoginForm(data: unknown): Result<LoginData, FormError> {
400
+ const errors: Record<string, string[]> = {};
401
+
402
+ if (!isValidEmail(data?.email)) {
403
+ errors.email = ["Invalid email format"];
404
+ }
405
+
406
+ if (Object.keys(errors).length > 0) {
407
+ return Err({
408
+ name: "FormError",
409
+ message: "Validation failed",
410
+ context: { fields: errors },
411
+ cause: undefined
412
+ });
413
+ }
414
+
415
+ return Ok(data as LoginData);
665
416
  }
666
-
667
- return oks.map(ok => ok.data); // Return processed content
668
417
  ```
418
+ </details>
669
419
 
670
- ### Real-World Example: Batch File Processing
671
-
672
- ```ts
673
- import { tryAsync, partitionResults } from "wellcrafted/result";
674
- import { type TaggedError } from "wellcrafted/error";
675
- import * as fs from 'fs/promises';
676
-
677
- type FileError = TaggedError<"FileError">;
678
-
679
- async function processFiles(filePaths: string[]) {
680
- // Map each file path to a Result-returning operation
681
- const results = await Promise.all(
682
- filePaths
683
- .map((path) => {
684
- if (!path.endsWith('.txt')) return; // Skip non-text files
685
- return tryAsync<string, FileError>({
686
- try: async () => {
687
- const content = await fs.readFile(path, 'utf-8');
688
- return content.toUpperCase(); // Process the content
689
- },
690
- mapError: (error) => ({
691
- name: "FileError",
692
- message: "Failed to process file",
693
- context: { path },
694
- cause: error
695
- })
696
- });
697
- })
698
- .filter((result) => result !== undefined)
699
- );
700
-
701
- const { oks, errs } = partitionResults(results);
420
+ <details>
421
+ <summary><b>React Hook</b></summary>
702
422
 
703
- // Report all errors together
704
- if (errs.length > 0) {
705
- console.error(`Failed to process ${errs.length} files:`);
706
- errs.forEach(err => {
707
- console.error(`- ${err.error.context.path}: ${err.error.message}`);
423
+ ```typescript
424
+ function useUser(id: number) {
425
+ const [state, setState] = useState<{
426
+ loading: boolean;
427
+ user?: User;
428
+ error?: ApiError;
429
+ }>({ loading: true });
430
+
431
+ useEffect(() => {
432
+ fetchUser(id).then(result => {
433
+ if (result.error) {
434
+ setState({ loading: false, error: result.error });
435
+ } else {
436
+ setState({ loading: false, user: result.data });
437
+ }
708
438
  });
709
- }
439
+ }, [id]);
710
440
 
711
- // Process successful results
712
- if (oks.length > 0) {
713
- console.log(`Successfully processed ${oks.length} files`);
714
- return oks.map(ok => ok.data); // Return processed content
715
- }
716
-
717
- return [];
441
+ return state;
718
442
  }
719
443
  ```
444
+ </details>
720
445
 
721
- ### Key Benefits
722
-
723
- 1. **Batch Error Handling**: Instead of stopping at the first error, you can collect all failures and present them together
724
- 2. **Type Safety**: The returned `oks` and `errs` arrays are properly typed as `Ok<T>[]` and `Err<E>[]` respectively
725
- 3. **Clean Separation**: Successful and failed operations are cleanly separated for different handling logic
726
- 4. **Composability**: Works seamlessly with the map β†’ filter β†’ partition pattern for complex data processing
446
+ ## Comparison with Alternatives
727
447
 
728
- ---
448
+ | | wellcrafted | fp-ts | Effect | neverthrow |
449
+ |---|---|---|---|---|
450
+ | **Learning Curve** | Minimal | Steep | Steep | Moderate |
451
+ | **Syntax** | Native async/await | Pipe operators | Generators | Method chains |
452
+ | **Bundle Size** | < 2KB | ~30KB | ~50KB | ~5KB |
453
+ | **Type Safety** | βœ… Full | βœ… Full | βœ… Full | βœ… Full |
454
+ | **Serializable Errors** | βœ… Built-in | ❌ Classes | ❌ Classes | ❌ Classes |
729
455
 
730
456
  ## API Reference
731
457
 
732
- ### Quick Reference Table
733
-
734
- | Function | Purpose | Example |
735
- |----------|---------|---------|
736
- | `Ok(data)` | Create success result | `Ok("hello")` |
737
- | `Err(error)` | Create failure result | `Err("failed")` |
738
- | `isOk(result)` | Check if success | `if (isOk(res)) { ... }` |
739
- | `isErr(result)` | Check if failure | `if (isErr(res)) { ... }` |
740
- | `trySync()` | Wrap throwing function | `trySync({ try: () => JSON.parse(str) })` |
741
- | `tryAsync()` | Wrap async throwing function | `tryAsync({ try: () => fetch(url) })` |
742
- | `partitionResults()` | Split Results into oks/errs | `const { oks, errs } = partitionResults(results)` |
743
-
744
- ### Detailed API
745
-
746
- #### Types
747
- - **`Result<T, E>`**: The core union type, representing `Ok<T> | Err<E>`.
748
- - **`Ok<T>`**: Represents a success. Contains `{ data: T; error: null; }`.
749
- - **`Err<E>`**: Represents a failure. Contains `{ data: null; error: E; }`.
750
- - **`BaseError` / `TaggedError<T>`**: Helpers for creating a structured error system.
751
-
752
- #### Core Result Functions
753
- - **`Ok(data)`**: Creates a success `Result`.
754
- - **`Err(error)`**: Creates a failure `Result`.
755
- - **`isOk(result)`**: Type guard. Returns `true` if the result is an `Ok` variant.
756
- - **`isErr(result)`**: Type guard. Returns `true` if the result is an `Err` variant.
757
- - **`unwrap(result)`**: Unwraps a `Result`, returning data on `Ok` or throwing error on `Err`.
758
- - **`resolve(value)`**: Resolves a value that may or may not be a `Result`, returning the final value or throwing on `Err`.
759
- - **`isResult(value)`**: Type guard. Returns `true` if a value has the shape of a `Result`.
760
-
761
- #### Async/Sync Wrappers
762
- - **`trySync({ try, mapError })`**: Wraps a synchronous function that may throw.
763
- - **`tryAsync({ try, mapError })`**: Wraps an asynchronous function that may throw or reject.
764
-
765
- #### Error Utilities
766
- - **`extractErrorMessage(error)`**: Safely extracts a string message from any error value.
767
-
768
- #### Utility Functions
769
- - **`partitionResults(results)`**: Partitions an array of Results into separate arrays of `Ok` and `Err` variants.
770
-
771
- ---
772
-
773
- ## Design Philosophy
774
-
775
- This library is built on a set of core principles designed to create a robust, predictable, and developer-friendly experience. Understanding these principles will help you get the most out of the library and see why its API is designed the way it is.
776
-
777
- ### 1. Embrace JavaScript Primitives
778
-
779
- A fundamental disagreement we have with some otherwise excellent libraries is the idea that JavaScript's core abstractions need to be completely reinvented. While we have immense respect for the power and type-level ingenuity of ecosystems like Effect-TS, we believe the cost of onboarding developers to an entirely new programming paradigm (like generators for async control flow) is too high for most projects.
780
-
781
- This library is built on the philosophy of leaning into JavaScript's native primitives whenever they are "good enough." We prefer to build on the familiar foundations of `async/await`, `Promise`, and standard union types (`T | null`) because they are already well-understood by the vast majority of TypeScript developers. This drastically reduces the learning curve and makes the library easy to adopt incrementally.
458
+ ### Result Functions
459
+ - **`Ok(data)`** - Create success result
460
+ - **`Err(error)`** - Create failure result
461
+ - **`isOk(result)`** - Type guard for success
462
+ - **`isErr(result)`** - Type guard for failure
463
+ - **`trySync(options)`** - Wrap throwing function
464
+ - **`tryAsync(options)`** - Wrap async function
465
+ - **`partitionResults(results)`** - Split array into oks/errs
782
466
 
783
- We only introduce new abstractions where JavaScript has a clear and significant weakness. In our view, the two biggest pain points in modern TypeScript are:
784
- 1. **Error Handling**: The imperative nature of `try/catch` and the non-serializable, class-based `Error` object.
785
- 2. **Data Validation**: Ensuring that `unknown` data conforms to a known type at runtime.
467
+ ### Error Functions
468
+ - **`createTaggedError(name)`** - Creates error factory functions
469
+ - Returns two functions: `{ErrorName}` and `{ErrorName}Err`
470
+ - The first creates plain error objects
471
+ - The second creates Err-wrapped errors
786
472
 
787
- This library provides `Result` to solve the first problem. It intentionally omits an `Option` type because native features like optional chaining (`?.`) and nullish coalescing (`??`) provide excellent and familiar ergonomics for handling optional values.
473
+ ### Types
474
+ - **`Result<T, E>`** - Union of Ok<T> | Err<E>
475
+ - **`TaggedError<T>`** - Structured error type
476
+ - **`Brand<T, B>`** - Branded type wrapper
788
477
 
789
- ### 2. Prioritize Ergonomics and Pragmatism
478
+ ## Learn More
790
479
 
791
- Flowing from the first principle, our API design prioritizes developer experience. This is most evident in our choice of the `{ data, error }` shape for the `Result` type. The ability to destructure `const { data, error } = ...` is a clean, direct, and pragmatic pattern that is already familiar to developers using popular libraries like Supabase and Astro Actions. We chose this pattern for its superior ergonomics, even if other patterns might be considered more "academically pure."
480
+ - πŸ“– [Full Documentation](https://github.com/your-repo/wellcrafted/wiki)
481
+ - πŸš€ [Examples](https://github.com/your-repo/wellcrafted/tree/main/examples)
482
+ - πŸ’¬ [Discussions](https://github.com/your-repo/wellcrafted/discussions)
792
483
 
793
- ### 3. Lightweight, Zero-Dependency, and Tree-Shakable
484
+ ## License
794
485
 
795
- This library is designed to be as lightweight as possible. It ships with **zero runtime dependencies**, meaning it won't add any extra weight to your `node_modules` folder or your final bundle.
796
-
797
- Every function is exported as a pure, standalone module, making the entire library **tree-shakable**. If you only use the `Result` type and the `isOk` function, the rest of the library's code won't be included in your application's build.
798
-
799
- We believe a library should have a focused scope and not be overwhelming. While comprehensive ecosystems like Effect-TS are incredibly powerful, their scope can be daunting. This library aims to solve the specific and critical problem of type-safe error handling without pulling in a large, all-encompassing framework. It's a small tool that does one job well.
800
-
801
- ### 4. Serialization-First
802
-
803
- A core requirement of this library is that all of its data structures, especially errors, must be reliably serializable. They need to behave identically whether you are passing them between functions, sending them over a network (HTTP), or passing them to a web worker. This is why the library fundamentally avoids classes for its error-handling system and instead promotes plain objects.
804
-
805
- ### 5. Opinionated yet Flexible
806
-
807
- This library is opinionated in that it provides a clear, recommended path for best practices. We believe that a degree of standardization leads to more maintainable and predictable codebases. However, these opinions are not enforced at a technical level. The core `Result` type is deliberately decoupled from the error system, meaning you are free to use a different error implementation if your project requires it.
808
-
809
- ## Inspirations and Relationship to Effect-TS
810
-
811
- This library's approach is heavily inspired by the powerful concepts pioneered by the **[Effect-TS](https://github.com/Effect-TS/effect)** ecosystem. Effect has indelibly shaped our thinking on how to structure services, handle errors, and compose applications in a type-safe way.
812
-
813
- However, this library represents a different set of trade-offs and priorities, based on a few key disagreements with the Effect-TS approach:
814
-
815
- 1. **Familiarity Over Novelty**: While we agree that Promises can be a flawed abstraction, we believe the cost of replacing them entirely is too high for most teams. Effect introduces a new, powerful, but unfamiliar execution model based on generators (`yield`), which requires a significant investment to learn. This library chooses to embrace the familiar patterns of `async/await` and Promises, even with their imperfections, to ensure a gentle learning curve. The goal is to provide 80% of the benefit with 20% of the learning curve.
816
-
817
- 2. **Simplicity and Lightweight Integration**: We aim for this library to be as lightweight as possible, easy to adopt incrementally, and simple to integrate with other tools. It is not an all-encompassing application framework but rather a focused tool to solve the specific problem of `Result`-based error handling.
818
-
819
- That said, the influence of Effect is clear. Functions like `trySync` and `tryAsync` are directly inspired by similar utilities in Effect. The core difference is that we aim to apply these powerful concepts on top of familiar JavaScript primitives, rather than creating a new ecosystem around them. This philosophy also informs our decision to omit an `Option<T>` type, as we believe that native TypeScript features (`T | null`, optional chaining, and nullish coalescing) are "good enough" and more idiomatic for the majority of use cases.
486
+ MIT
820
487
 
821
488
  ---
822
489
 
823
- ## FAQ
824
-
825
- ### Why `{ data, error }` instead of a boolean flag like `{ ok: boolean, ... }`?
826
-
827
- Some libraries use a boolean flag for their discriminated union, like `{ ok: true, data: T } | { ok: false, error: E }`. While a valid pattern, we chose the `{ data, error }` shape for two main reasons:
828
-
829
- 1. **Ergonomics and Familiarity**: The destructuring pattern `const { data, error } = operation()` is clean and will feel familiar to developers using modern libraries like Supabase and Astro Actions. It provides immediate access to the inner values without an extra layer of property access. Checking a boolean flag first (`if (result.ok)`) and then accessing the value (`result.data`) is slightly more verbose.
830
-
831
- 2. **Lack of Standardization**: The boolean flag approach isn't standardized. Zod's `.safeParse`, for example, returns `{ success: boolean, ... }`. By adopting the `{ data, error }` pattern, we align with a simple, common, and intuitive structure for handling success and failure states in modern JavaScript.
832
-
833
- **Note**: The `{ data, error }` pattern is also a discriminated unionβ€”you can use either `data` or `error` as the discriminant key and check if either of them is null. This creates the same type-narrowing benefits as a boolean flag while maintaining cleaner destructuring ergonomics.
834
-
835
- ### What's the difference between an `Err` variant and an `error` value?
836
-
837
- This is a key distinction in the library's terminology:
838
-
839
- - **`Err<E>` (The Variant/Container)**: This is one of the two possible "shapes" of a `Result` object. It's the wrapper itself, whose structure is `{ data: null, error: E }`. You can think of it as the box that signifies a failure.
840
-
841
- - **`error` (The Value/Payload)**: This is the actual *value* inside the `Err` container. It is the content of the `error` property on the `Err` object. This is the piece of data that describes what went wrong, and its type is `E`.
842
-
843
- When you use the `isErr()` type guard, you are checking if a `Result` is the `Err` variant. Once that check passes, you can then access the `.error` property to get the error value.
844
-
845
- ### Why doesn't this library include an `Option<T>` type?
846
-
847
- An `Option<T>` type (sometimes called `Maybe`) is common in other languages to represent a value that might be missing. However, we've intentionally omitted it because **modern JavaScript and TypeScript already have excellent, first-class support for handling potentially missing values.**
848
-
849
- A custom `Option<T>` type would add a layer of abstraction that is largely unnecessary. Instead, you can and should use:
850
-
851
- 1. **Union Types with `null`**: Simply type your value as `T | null`. This is the idiomatic way to represent an optional value in TypeScript.
852
-
853
- 2. **Optional Chaining (`?.`)**: Safely access nested properties of an object that might be null or undefined.
854
- ```ts
855
- const street = user?.address?.street; // Returns undefined if user or address is null/undefined
856
- ```
857
-
858
- 3. **Nullish Coalescing (`??`)**: Provide a default value for a `null` or `undefined` expression.
859
- ```ts
860
- const displayName = user.name ?? "Guest";
861
- ```
862
-
863
- These built-in language features provide better ergonomics and are more familiar to JavaScript developers than a custom `Option` type would be. This library focuses on solving for `Result`, where the language does not have a built-in equivalent.
490
+ Made with ❀️ by developers who believe error handling should be delightful.