wellcrafted 0.18.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,733 +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. Function inputs & debugging data (optional)
212
- cause: unknown; // 4. Root cause
213
- };
214
- ```
124
+ **🔍 Zero Magic**
125
+ ~50 lines of core code
215
126
 
216
- > The `context` property should include the function's input parameters and any relevant variables in the closure. If there are none, then it can be omitted. This creates a complete picture of what data led to the error, making debugging straightforward.
127
+ </td>
128
+ <td>
217
129
 
218
- #### 1. **`name`** - The Discriminant (Tagged Field)
219
-
220
- 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:
221
-
222
- ```ts
223
- type ValidationError = TaggedError<"ValidationError">;
224
- type NetworkError = TaggedError<"NetworkError">;
225
- type FileError = TaggedError<"FileError">;
226
-
227
- function handleError(error: ValidationError | NetworkError | FileError) {
228
- switch (error.name) {
229
- case "ValidationError":
230
- // TypeScript knows this is ValidationError
231
- console.log("Invalid input:", error.context);
232
- break;
233
- case "NetworkError":
234
- // TypeScript knows this is NetworkError
235
- console.log("Network failed:", error.message);
236
- break;
237
- case "FileError":
238
- // TypeScript knows this is FileError
239
- console.log("File issue:", error.context);
240
- break;
241
- }
242
- }
243
- ```
130
+ **🚀 Lightweight**
131
+ Zero dependencies, < 2KB
244
132
 
245
- #### 2. **`message`** - Human-Readable Text
133
+ </td>
134
+ <td>
246
135
 
247
- Pure text description that explains what went wrong. Keep it clear and actionable:
136
+ **🎨 Composable**
137
+ Mix and match utilities
248
138
 
249
- ```ts
250
- return Err({
251
- name: "ValidationError",
252
- message: "Email address must contain an @ symbol", // Clear, specific
253
- context: { email: userInput },
254
- cause: undefined
255
- });
256
- ```
139
+ </td>
140
+ </tr>
141
+ </table>
257
142
 
258
- #### 3. **`context`** - Function Inputs & Debugging Data
143
+ ## The Result Pattern Explained
259
144
 
260
- The primary purpose of `context` is to capture the function's input parameters, relevant variables in the closure, and additional context.
145
+ The Result type makes error handling explicit and type-safe:
261
146
 
262
- ```ts
263
- function processUser(id: number, options: UserOptions): Result<User, ProcessError> {
264
- return Err({
265
- name: "ProcessError",
266
- message: "User processing failed",
267
- context: {
268
- userId: id, // Function input
269
- options, // Function input
270
- timestamp: new Date().toISOString(), // Additional context
271
- retryCount: 3 // Useful debugging info
272
- },
273
- cause: undefined
274
- });
275
- }
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>;
276
152
  ```
277
153
 
278
- #### 4. **`cause`** - Root Cause Bubbling
154
+ **The Magic**: This creates a discriminated union where TypeScript automatically narrows types:
279
155
 
280
- - **For new errors**: Set `cause: undefined`
281
- - **For wrapping existing errors**: Pass the original error as `cause`
282
-
283
- ```ts
284
- // Creating a new error
285
- return Err({
286
- name: "ValidationError",
287
- message: "Invalid user data",
288
- context: { input },
289
- cause: undefined // New error, no underlying cause
290
- });
291
-
292
- // Wrapping an existing error
293
- try {
294
- await database.save(user);
295
- } catch (dbError) {
296
- return Err({
297
- name: "SaveError",
298
- message: "Failed to save user",
299
- context: { userId: user.id },
300
- cause: dbError // Bubble up the original database error
301
- });
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
302
161
  }
303
162
  ```
304
163
 
305
- ### Creating Domain-Specific Errors
164
+ ## Basic Patterns
306
165
 
307
- You can define a set of possible errors for a specific domain:
166
+ ### Handle Results with Destructuring
308
167
 
309
168
  ```typescript
310
- // Define your specific error types
311
- export type FileNotFoundError = TaggedError<"FileNotFoundError">;
312
- export type PermissionDeniedError = TaggedError<"PermissionDeniedError">;
313
- export type DiskFullError = TaggedError<"DiskFullError">;
314
-
315
- // Create a union of all possible errors for this domain
316
- export type FileSystemError = FileNotFoundError | PermissionDeniedError | DiskFullError;
169
+ const { data, error } = await someOperation();
317
170
 
318
- // A factory function to create an error
319
- function createFileNotFoundError(path: string, cause?: unknown): FileNotFoundError {
320
- return {
321
- name: "FileNotFoundError",
322
- message: `The file at path "${path}" was not found.`,
323
- context: { path },
324
- cause
325
- };
171
+ if (error) {
172
+ // Handle error with full type safety
173
+ return;
326
174
  }
327
- ```
328
-
329
- Because `name` is a unique literal type for each error, TypeScript can use it to discriminate between them in a `switch` statement:
330
175
 
331
- ```ts
332
- function handleError(error: FileSystemError) {
333
- switch (error.name) {
334
- case "FileNotFoundError":
335
- // TypeScript knows `error` is `FileNotFoundError` here.
336
- console.error(`Path not found: ${error.context.path}`);
337
- break;
338
- case "PermissionDeniedError":
339
- // TypeScript knows `error` is `PermissionDeniedError` here.
340
- console.error("Permission was denied.");
341
- break;
342
- case "DiskFullError":
343
- // ...
344
- break;
345
- }
346
- }
176
+ // Use data - TypeScript knows it's safe
347
177
  ```
348
178
 
349
- ### Best Practices for Errors
350
-
351
- #### 1. Include Meaningful Context
352
- Always include function inputs and other relevant state in the `context` object. This is invaluable for logging and debugging.
179
+ ### Wrap Unsafe Operations
353
180
 
354
181
  ```typescript
355
- function createDbError(
356
- message: string,
357
- query: string,
358
- params: unknown[],
359
- cause: unknown
360
- ): DbError {
361
- return {
362
- name: "DbError",
363
- message,
364
- context: {
365
- query,
366
- params,
367
- timestamp: new Date().toISOString(),
368
- },
369
- cause,
370
- };
371
- }
372
- ```
373
-
374
- #### 2. Handle Errors at the Right Level
375
- Handle or transform errors where you can add more context or make a recovery decision.
376
-
377
- ```ts
378
- async function initializeApp(): Promise<Result<App, FsError | ValidationError>> {
379
- const configResult = await readConfig("./config.json");
380
-
381
- // Propagate the file system error directly if config read fails
382
- if (isErr(configResult)) {
383
- return configResult;
384
- }
385
-
386
- // If config is read, but is invalid, return a *different* kind of error
387
- const validationResult = validateConfig(configResult.data);
388
- if (isErr(validationResult)) {
389
- return validationResult;
390
- }
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
+ });
391
192
 
392
- return Ok(new App(validationResult.data));
393
- }
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
+ });
394
203
  ```
395
204
 
396
- This structure makes errors **trackable**, **debuggable**, and **type-safe** while maintaining clean separation between different failure modes in your application.
397
-
398
- ---
399
-
400
- ## Basic Usage
401
-
402
- Now that you understand how to handle Result values and the TaggedError structure, let's see complete examples that combine both concepts:
205
+ ### Service Layer Example
403
206
 
404
- ```ts
405
- import { Result, Ok, Err, isOk } from "wellcrafted/result";
406
- import { type TaggedError } from "wellcrafted/error";
407
-
408
- // --- Example 1: A Safe Division Function ---
207
+ ```typescript
208
+ import { Result, Ok, tryAsync } from "wellcrafted/result";
209
+ import { createTaggedError } from "wellcrafted/error";
409
210
 
410
- // 1. Define a specific error for math-related failures
411
- type MathError = TaggedError<"MathError">;
211
+ // Define service-specific errors
212
+ const { ValidationError, ValidationErr } = createTaggedError("ValidationError");
213
+ const { DatabaseError, DatabaseErr } = createTaggedError("DatabaseError");
412
214
 
413
- // 2. Create a function that returns a Result with our structured error
414
- function divide(numerator: number, denominator: number): Result<number, MathError> {
415
- if (denominator === 0) {
416
- return Err({
417
- name: "MathError",
418
- message: "Cannot divide by zero.",
419
- context: { numerator, denominator },
420
- cause: undefined
421
- });
422
- }
423
- return Ok(numerator / denominator);
424
- }
215
+ type ValidationError = ReturnType<typeof ValidationError>;
216
+ type DatabaseError = ReturnType<typeof DatabaseError>;
425
217
 
426
- // 3. Handle the result
427
- 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
+ }
428
230
 
429
- if (!isOk(divisionResult)) {
430
- // `divisionResult.error` is a fully-typed MathError object
431
- console.error(`Error (${divisionResult.error.name}): ${divisionResult.error.message}`);
432
- console.log("Context:", divisionResult.error.context); // { numerator: 10, denominator: 0 }
433
- }
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
+ },
434
240
 
435
- // --- Example 2: Parsing a User Object ---
436
-
437
- // 1. Define a specific error for parsing failures
438
- type ParseError = TaggedError<"ParseError">;
439
-
440
- // 2. Create a function that returns a Result with our structured error
441
- function parseUser(json: string): Result<{ name: string }, ParseError> {
442
- try {
443
- const data = JSON.parse(json);
444
- if (typeof data.name !== "string") {
445
- return Err({
446
- name: "ParseError",
447
- message: "User object must have a name property of type string.",
448
- context: { receivedValue: data.name },
449
- 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
+ })
450
249
  });
451
250
  }
452
- return Ok(data);
453
- } catch (e) {
454
- return Err({
455
- name: "ParseError",
456
- message: "Invalid JSON provided.",
457
- context: { rawString: json },
458
- cause: e,
459
- });
460
- }
251
+ };
461
252
  }
462
253
 
463
- // 3. Handle the result
464
- const userResult = parseUser('{"name": "Alice"}');
254
+ // Export type for the service
255
+ export type UserService = ReturnType<typeof createUserService>;
465
256
 
466
- if (isOk(userResult)) {
467
- console.log(`Welcome, ${userResult.data.name}!`);
468
- } else {
469
- // `userResult.error` is a fully-typed ParseError object
470
- console.error(`Error (${userResult.error.name}): ${userResult.error.message}`);
471
- console.log("Context:", userResult.error.context);
472
- }
257
+ // Create a live instance (dependency injection at build time)
258
+ export const UserServiceLive = createUserService(databaseInstance);
473
259
  ```
474
260
 
475
- ---
476
-
477
- ## Wrapping Functions That Throw
261
+ ## Why wellcrafted?
478
262
 
479
- 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:
480
264
 
481
- ### 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)
482
269
 
483
- 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
484
275
 
485
- ```ts
486
- import { trySync, Result } from "wellcrafted/result";
487
- import { type TaggedError } from "wellcrafted/error";
276
+ ## Service Pattern Best Practices
488
277
 
489
- type ParseError = TaggedError<"ParseError">;
278
+ Based on real-world usage, here's the recommended pattern for creating services with wellcrafted:
490
279
 
491
- function parseJson(raw: string): Result<object, ParseError> {
492
- return trySync({
493
- try: () => JSON.parse(raw),
494
- mapError: (error) => ({
495
- name: "ParseError",
496
- message: "Failed to parse JSON",
497
- context: { raw },
498
- cause: error
499
- })
500
- });
501
- }
280
+ ### Factory Function Pattern
502
281
 
503
- const result = parseJson('{"key": "value"}'); // Ok<{key: string}>
504
- const failedResult = parseJson('not json'); // Err<ParseError>
505
- ```
506
-
507
- ### Asynchronous Operations with `tryAsync`
508
-
509
- Use `tryAsync` for functions that return a `Promise`. It handles both rejected promises and synchronous throws within the async function.
510
-
511
- ```ts
512
- import { tryAsync, Result } from "wellcrafted/result";
513
- import { type TaggedError } from "wellcrafted/error";
514
-
515
- type User = { id: number; name: string };
516
- type NetworkError = TaggedError<"NetworkError">;
517
-
518
- async function fetchUser(userId: number): Promise<Result<User, NetworkError>> {
519
- return tryAsync({
520
- try: async () => {
521
- const response = await fetch(`https://api.example.com/users/${userId}`);
522
- if (!response.ok) {
523
- // You can throw a custom error object
524
- 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
+ });
525
303
  }
526
- return response.json();
304
+
305
+ isRecording = true;
306
+ return Ok(undefined);
527
307
  },
528
- mapError: (error) => ({
529
- name: "NetworkError",
530
- message: "Failed to fetch user",
531
- context: { userId },
532
- cause: error
533
- })
534
- });
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
+ };
535
322
  }
536
323
 
537
- const userResult = await fetchUser(1);
538
- ```
324
+ // 3. Export type
325
+ export type RecorderService = ReturnType<typeof createRecorderService>;
539
326
 
540
- ### Type Safety with Generics
327
+ // 4. Create singleton instance
328
+ export const RecorderServiceLive = createRecorderService();
329
+ ```
541
330
 
542
- When using `trySync` and `tryAsync`, you have two approaches to ensure your `mapError` function returns the correct TaggedError type:
331
+ ### Platform-Specific Services
543
332
 
544
- #### Approach 1: Explicit Generics
333
+ For services that need different implementations per platform:
545
334
 
546
- 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
+ };
547
341
 
548
- ```ts
549
- // For tryAsync
550
- async function readConfig(path: string) {
551
- return tryAsync<string, FileError>({ // 👈 <SuccessType, ErrorType>
552
- try: async () => {
553
- const content = await fs.readFile(path, 'utf-8');
554
- return content;
342
+ // desktop.ts
343
+ export function createFileServiceDesktop(): FileService {
344
+ return {
345
+ async readFile(path) {
346
+ // Desktop implementation using Node.js APIs
555
347
  },
556
- mapError: (error) => ({
557
- name: "FileError",
558
- message: "Failed to read configuration file",
559
- context: { path },
560
- cause: error
561
- })
562
- });
348
+ async writeFile(path, content) {
349
+ // Desktop implementation
350
+ }
351
+ };
563
352
  }
564
353
 
565
- // For trySync
566
- function parseConfig(content: string) {
567
- return trySync<Config, ParseError>({ // 👈 <SuccessType, ErrorType>
568
- try: () => {
569
- const parsed = JSON.parse(content);
570
- validateConfig(parsed); // throws if invalid
571
- return parsed as Config;
354
+ // web.ts
355
+ export function createFileServiceWeb(): FileService {
356
+ return {
357
+ async readFile(path) {
358
+ // Web implementation using File API
572
359
  },
573
- mapError: (error) => ({
574
- name: "ParseError",
575
- message: "Invalid configuration format",
576
- context: { contentLength: content.length },
577
- cause: error
578
- })
579
- });
360
+ async writeFile(path, content) {
361
+ // Web implementation
362
+ }
363
+ };
580
364
  }
365
+
366
+ // index.ts - runtime selection
367
+ export const FileServiceLive = typeof window !== 'undefined'
368
+ ? createFileServiceWeb()
369
+ : createFileServiceDesktop();
581
370
  ```
582
371
 
583
- #### Approach 2: Return Type Annotation on mapError
372
+ ## Common Use Cases
584
373
 
585
- Annotate the return type of the `mapError` function. This approach can be cleaner when generics would format awkwardly:
374
+ <details>
375
+ <summary><b>API Route Handler</b></summary>
586
376
 
587
- ```ts
588
- // For tryAsync
589
- async function saveUser(user: User) {
590
- return tryAsync({
591
- try: async () => {
592
- const result = await db.users.insert(user);
593
- return result.id;
594
- },
595
- mapError: (error): DatabaseError => ({ // 👈 Annotate return type
596
- name: "DatabaseError",
597
- message: "Failed to save user to database",
598
- context: { userId: user.id },
599
- cause: error
600
- })
601
- });
602
- }
603
-
604
- // For trySync
605
- function validateEmail(email: string) {
606
- return trySync({
607
- try: () => {
608
- if (!email.includes('@')) {
609
- throw new Error('Invalid email format');
610
- }
611
- return email.toLowerCase();
612
- },
613
- mapError: (error): ValidationError => ({ // 👈 Annotate return type
614
- name: "ValidationError",
615
- message: "Email validation failed",
616
- context: { email },
617
- cause: error
618
- })
619
- });
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);
620
391
  }
621
392
  ```
393
+ </details>
622
394
 
623
- **Key points:**
624
- - Both approaches ensure `mapError` returns your exact TaggedError type
625
- - Avoid using `as const` - always map to proper TaggedError objects
626
- - Choose explicit generics for clarity, or return type annotation for brevity
627
- - The important thing is ensuring type safety for your error handling
628
-
629
- ---
630
-
631
- ## Partitioning Results
632
-
633
- 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.
395
+ <details>
396
+ <summary><b>Form Validation</b></summary>
634
397
 
635
- ### When to Use `partitionResults`
636
-
637
- Use `partitionResults` when you have:
638
- - Multiple async operations that might fail independently
639
- - A need to handle all errors collectively
640
- - Successful results that should be processed together
641
-
642
- ### Common Pattern: Map → Filter → Partition
643
-
644
- This is a typical workflow when processing multiple commands or operations:
645
-
646
- ```ts
647
- import { partitionResults } from "wellcrafted/result";
648
-
649
- // Example: Processing multiple commands that might fail
650
- const results = await Promise.all(
651
- commands
652
- .map((command) => {
653
- const config = getCommandConfig(command.id);
654
- if (!config) return; // Early return if config missing
655
- return executeCommand({ command, config });
656
- })
657
- .filter((result) => result !== undefined) // Remove undefined values
658
- );
659
-
660
- const { oks, errs } = partitionResults(results);
661
-
662
- // Handle all errors at once
663
- if (errs.length > 0) {
664
- const errorMessages = errs.map(({ error }) => error.message).join(', ');
665
- showNotification(`${errs.length} operations failed: ${errorMessages}`);
666
- 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);
667
416
  }
668
-
669
- return oks.map(ok => ok.data); // Return processed content
670
417
  ```
418
+ </details>
671
419
 
672
- ### Real-World Example: Batch File Processing
673
-
674
- ```ts
675
- import { tryAsync, partitionResults } from "wellcrafted/result";
676
- import { type TaggedError } from "wellcrafted/error";
677
- import * as fs from 'fs/promises';
678
-
679
- type FileError = TaggedError<"FileError">;
680
-
681
- async function processFiles(filePaths: string[]) {
682
- // Map each file path to a Result-returning operation
683
- const results = await Promise.all(
684
- filePaths
685
- .map((path) => {
686
- if (!path.endsWith('.txt')) return; // Skip non-text files
687
- return tryAsync<string, FileError>({
688
- try: async () => {
689
- const content = await fs.readFile(path, 'utf-8');
690
- return content.toUpperCase(); // Process the content
691
- },
692
- mapError: (error) => ({
693
- name: "FileError",
694
- message: "Failed to process file",
695
- context: { path },
696
- cause: error
697
- })
698
- });
699
- })
700
- .filter((result) => result !== undefined)
701
- );
702
-
703
- const { oks, errs } = partitionResults(results);
420
+ <details>
421
+ <summary><b>React Hook</b></summary>
704
422
 
705
- // Report all errors together
706
- if (errs.length > 0) {
707
- console.error(`Failed to process ${errs.length} files:`);
708
- errs.forEach(err => {
709
- 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
+ }
710
438
  });
711
- }
439
+ }, [id]);
712
440
 
713
- // Process successful results
714
- if (oks.length > 0) {
715
- console.log(`Successfully processed ${oks.length} files`);
716
- return oks.map(ok => ok.data); // Return processed content
717
- }
718
-
719
- return [];
441
+ return state;
720
442
  }
721
443
  ```
444
+ </details>
722
445
 
723
- ### Key Benefits
446
+ ## Comparison with Alternatives
724
447
 
725
- 1. **Batch Error Handling**: Instead of stopping at the first error, you can collect all failures and present them together
726
- 2. **Type Safety**: The returned `oks` and `errs` arrays are properly typed as `Ok<T>[]` and `Err<E>[]` respectively
727
- 3. **Clean Separation**: Successful and failed operations are cleanly separated for different handling logic
728
- 4. **Composability**: Works seamlessly with the map filter partition pattern for complex data processing
729
-
730
- ---
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 |
731
455
 
732
456
  ## API Reference
733
457
 
734
- ### Quick Reference Table
735
-
736
- | Function | Purpose | Example |
737
- |----------|---------|---------|
738
- | `Ok(data)` | Create success result | `Ok("hello")` |
739
- | `Err(error)` | Create failure result | `Err("failed")` |
740
- | `isOk(result)` | Check if success | `if (isOk(res)) { ... }` |
741
- | `isErr(result)` | Check if failure | `if (isErr(res)) { ... }` |
742
- | `trySync()` | Wrap throwing function | `trySync({ try: () => JSON.parse(str) })` |
743
- | `tryAsync()` | Wrap async throwing function | `tryAsync({ try: () => fetch(url) })` |
744
- | `partitionResults()` | Split Results into oks/errs | `const { oks, errs } = partitionResults(results)` |
745
-
746
- ### Detailed API
747
-
748
- #### Types
749
- - **`Result<T, E>`**: The core union type, representing `Ok<T> | Err<E>`.
750
- - **`Ok<T>`**: Represents a success. Contains `{ data: T; error: null; }`.
751
- - **`Err<E>`**: Represents a failure. Contains `{ data: null; error: E; }`.
752
- - **`BaseError` / `TaggedError<T>`**: Helpers for creating a structured error system.
753
-
754
- #### Core Result Functions
755
- - **`Ok(data)`**: Creates a success `Result`.
756
- - **`Err(error)`**: Creates a failure `Result`.
757
- - **`isOk(result)`**: Type guard. Returns `true` if the result is an `Ok` variant.
758
- - **`isErr(result)`**: Type guard. Returns `true` if the result is an `Err` variant.
759
- - **`unwrap(result)`**: Unwraps a `Result`, returning data on `Ok` or throwing error on `Err`.
760
- - **`resolve(value)`**: Resolves a value that may or may not be a `Result`, returning the final value or throwing on `Err`.
761
- - **`isResult(value)`**: Type guard. Returns `true` if a value has the shape of a `Result`.
762
-
763
- #### Async/Sync Wrappers
764
- - **`trySync({ try, mapError })`**: Wraps a synchronous function that may throw.
765
- - **`tryAsync({ try, mapError })`**: Wraps an asynchronous function that may throw or reject.
766
-
767
- #### Error Utilities
768
- - **`extractErrorMessage(error)`**: Safely extracts a string message from any error value.
769
-
770
- #### Utility Functions
771
- - **`partitionResults(results)`**: Partitions an array of Results into separate arrays of `Ok` and `Err` variants.
772
-
773
- ---
774
-
775
- ## Design Philosophy
776
-
777
- 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.
778
-
779
- ### 1. Embrace JavaScript Primitives
780
-
781
- 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.
782
-
783
- 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
784
466
 
785
- 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:
786
- 1. **Error Handling**: The imperative nature of `try/catch` and the non-serializable, class-based `Error` object.
787
- 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
788
472
 
789
- 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
790
477
 
791
- ### 2. Prioritize Ergonomics and Pragmatism
478
+ ## Learn More
792
479
 
793
- 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)
794
483
 
795
- ### 3. Lightweight, Zero-Dependency, and Tree-Shakable
484
+ ## License
796
485
 
797
- 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.
798
-
799
- 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.
800
-
801
- 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.
802
-
803
- ### 4. Serialization-First
804
-
805
- 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.
806
-
807
- ### 5. Opinionated yet Flexible
808
-
809
- 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.
810
-
811
- ## Inspirations and Relationship to Effect-TS
812
-
813
- 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.
814
-
815
- However, this library represents a different set of trade-offs and priorities, based on a few key disagreements with the Effect-TS approach:
816
-
817
- 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.
818
-
819
- 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.
820
-
821
- 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
822
487
 
823
488
  ---
824
489
 
825
- ## FAQ
826
-
827
- ### Why `{ data, error }` instead of a boolean flag like `{ ok: boolean, ... }`?
828
-
829
- 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:
830
-
831
- 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.
832
-
833
- 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.
834
-
835
- **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.
836
-
837
- ### What's the difference between an `Err` variant and an `error` value?
838
-
839
- This is a key distinction in the library's terminology:
840
-
841
- - **`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.
842
-
843
- - **`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`.
844
-
845
- 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.
846
-
847
- ### Why doesn't this library include an `Option<T>` type?
848
-
849
- 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.**
850
-
851
- A custom `Option<T>` type would add a layer of abstraction that is largely unnecessary. Instead, you can and should use:
852
-
853
- 1. **Union Types with `null`**: Simply type your value as `T | null`. This is the idiomatic way to represent an optional value in TypeScript.
854
-
855
- 2. **Optional Chaining (`?.`)**: Safely access nested properties of an object that might be null or undefined.
856
- ```ts
857
- const street = user?.address?.street; // Returns undefined if user or address is null/undefined
858
- ```
859
-
860
- 3. **Nullish Coalescing (`??`)**: Provide a default value for a `null` or `undefined` expression.
861
- ```ts
862
- const displayName = user.name ?? "Guest";
863
- ```
864
-
865
- 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.