@iahuang/result-ts 1.2.0 → 2.0.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.
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(npm run build:*)"
4
+ "Bash(npm run build:*)",
5
+ "Bash(npx tsc:*)",
6
+ "Bash(npm test:*)",
7
+ "Bash(npm install:*)"
5
8
  ]
6
9
  }
7
10
  }
package/README.md CHANGED
@@ -5,7 +5,7 @@ A TypeScript implementation of Rust's [`std::result::Result`](https://doc.rust-l
5
5
  This library provides a lightweight, type-safe way to handle errors without exceptions. It aims to:
6
6
  - Stay close to Rust's original API design
7
7
  - Provide strict type safety with full TypeScript inference
8
- - Use plain objects (not classes) for easy serialization
8
+ - Use plain objects as opposed to classes to allow for JSON serialization
9
9
  - Support both synchronous and asynchronous operations
10
10
 
11
11
  ## Installation
@@ -16,58 +16,38 @@ npm install @iahuang/result-ts
16
16
 
17
17
  ## Quick Start
18
18
 
19
+ Define your error types, then use `resultType()` to create a type-safe result builder:
20
+
19
21
  ```ts
22
+ import { resultType, chain, match } from '@iahuang/result-ts';
23
+
20
24
  type ParseErrors = {
21
- invalid_number: string; // the string that failed to parse as a number
22
- negative: number; // the number that was negative
25
+ invalid_number: string;
26
+ negative: number;
23
27
  };
24
28
 
25
29
  const parseResult = resultType<number, ParseErrors>();
26
30
 
27
- type ParseError = Err<ParseErrors>; // { ok: false, error: "invalid_number", detail: string } | { ok: false, error: "negative", detail: number }
28
- type ParseOk = Ok<number>; // { ok: true, value: number }
29
-
30
- type ParseResult = ResultType<typeof parseResult>; // ParseError | ParseOk
31
-
32
- function parseIntStrict(s: string): ParseResult {
31
+ function parsePositiveInt(s: string): Result<number, ParseErrors> {
33
32
  const n = Number.parseInt(s, 10);
34
33
  if (Number.isNaN(n)) return parseResult.err("invalid_number", s);
35
34
  if (n < 0) return parseResult.err("negative", n);
36
35
  return parseResult.ok(n);
37
36
  }
38
-
39
- const timesTwo = resultMap(parseIntStrict("123"), (n) => n * 2);
40
-
41
- // construct plain results manually
42
- const okResult = parseResult.ok(123); // { ok: true, value: 123 }
43
- const errResult = parseResult.err("invalid_number", "123"); // { ok: false, error: "invalid_number", detail: "123" }
44
37
  ```
45
38
 
46
- The equivalent Rust code is:
47
-
48
- ```rust
49
- enum ParseError {
50
- InvalidNumber(String),
51
- Negative(i32),
52
- }
53
-
54
- fn parse_int_strict(s: &str) -> Result<i32, ParseError> {
55
- match s.parse::<i32>() {
56
- Ok(n) => {
57
- if n < 0 {
58
- Err(ParseError::Negative(n))
59
- } else {
60
- Ok(n)
61
- }
62
- },
63
- Err(_) => Err(ParseError::InvalidNumber(s.to_string())),
64
- }
65
- }
39
+ Chain operations and handle results:
66
40
 
67
- let times_two = parse_int_strict("123").map(|n| n * 2);
41
+ ```ts
42
+ const value = chain(parsePositiveInt("42"))
43
+ .map((n) => n * 2)
44
+ .unwrapOr(0);
68
45
 
69
- let ok_result = Ok(123);
70
- let err_result = Err(ParseError::InvalidNumber("123".to_string()));
46
+ // Or use pattern matching for full control
47
+ match(parsePositiveInt("-5"), {
48
+ ok: (n) => console.log(`Got ${n}`),
49
+ err: (e) => console.log(`Error: ${e.type}`)
50
+ });
71
51
  ```
72
52
 
73
53
  ## Core Concepts
@@ -75,22 +55,26 @@ let err_result = Err(ParseError::InvalidNumber("123".to_string()));
75
55
  ### Result Types
76
56
 
77
57
  A `Result<T, E>` is either:
78
- - `Ok<T>`: Success, containing a value of type `T`
58
+ - Success, containing a value of type `T`
79
59
  ```ts
80
60
  { ok: true, value: T }
81
61
  ```
82
- - `Err<E>`: Failure, containing an error variant and detail
62
+ - Failure, containing an `Err<E>` under the `error` field
83
63
  ```ts
84
- { ok: false, error: keyof E, detail: E[keyof E] }
64
+ { ok: false, error: Err<E> }
85
65
  ```
86
66
 
67
+ Where `Err<E>` is a discriminated union of `{ type: K, detail: E[K] }` for each key `K` in `E`.
68
+
69
+ This is analogous to creating a `Result<T, E>` type in Rust, with `E` being an enum type with variants for different error types. The constraint that `E` be an enum-like type is baked into this library's type system to encourage the implementation of fixed, statically-known sets of error types which can each be handled appropriately.
70
+
87
71
  ### Creating Results
88
72
 
89
73
  You can create results in two ways:
90
74
 
91
- #### 1. Using `resultType()` (Recommended)
75
+ #### 1. **Recommended:** Using `resultType()`
92
76
 
93
- This provides full type safety and inference:
77
+ This method provides the strongest type safety and inference.
94
78
 
95
79
  ```ts
96
80
  type MyErrors = {
@@ -129,7 +113,21 @@ import { match } from '@iahuang/result-ts';
129
113
 
130
114
  const message = match(parsePositiveInt("123"), {
131
115
  ok: (value) => `Success: ${value}`,
132
- err: (e) => `Error: ${e.error} - ${e.detail}`
116
+ err: (e) => `Error: ${e.type} - ${e.detail}`
117
+ });
118
+ ```
119
+
120
+ For exhaustive matching on individual error variants, use `matchNested`:
121
+
122
+ ```ts
123
+ import { matchNested } from '@iahuang/result-ts';
124
+
125
+ const message = matchNested(parsePositiveInt("123"), {
126
+ ok: (value) => `Success: ${value}`,
127
+ err: {
128
+ invalid_number: (s) => `Invalid number: ${s}`,
129
+ negative: (n) => `Negative number: ${n}`,
130
+ }
133
131
  });
134
132
  ```
135
133
 
@@ -218,10 +216,10 @@ resultInspectErr(result, (err) => {
218
216
  All Result functions have async versions for working with Promises:
219
217
 
220
218
  ```ts
221
- import { asyncResultMap, asyncResultAndThen, tryCatchResultAsync } from '@iahuang/result-ts';
219
+ import { asyncResultMap, asyncResultAndThen, resultFromThrowingAsyncFunction } from '@iahuang/result-ts';
222
220
 
223
221
  async function fetchUser(id: string): Promise<Result<User, FetchErrors>> {
224
- return tryCatchResultAsync(
222
+ return resultFromThrowingAsyncFunction(
225
223
  async () => {
226
224
  const response = await fetch(`/api/users/${id}`);
227
225
  return await response.json();
@@ -249,7 +247,7 @@ const email = await asyncChain(fetchUser("123"))
249
247
  Convert throwing code to Results:
250
248
 
251
249
  ```ts
252
- import { tryCatchResult } from '@iahuang/result-ts';
250
+ import { resultFromThrowingFunction, resultType, Result } from '@iahuang/result-ts';
253
251
 
254
252
  type JsonErrors = {
255
253
  parse_error: string;
@@ -257,8 +255,8 @@ type JsonErrors = {
257
255
 
258
256
  const jsonResult = resultType<any, JsonErrors>();
259
257
 
260
- function parseJSON(jsonString: string): ResultType<typeof jsonResult> {
261
- return tryCatchResult(
258
+ function parseJSON(jsonString: string): Result<any, JsonErrors> {
259
+ return resultFromThrowingFunction(
262
260
  () => JSON.parse(jsonString),
263
261
  (e) => jsonResult.err("parse_error", String(e))
264
262
  );
@@ -268,73 +266,41 @@ const data = parseJSON('{"valid": "json"}');
268
266
  // Ok({ valid: "json" })
269
267
 
270
268
  const invalid = parseJSON('invalid json');
271
- // Err({ ok: false, error: "parse_error", detail: "SyntaxError: ..." })
269
+ // Err({ ok: false, error: { type: "parse_error", detail: "SyntaxError: ..." } })
272
270
  ```
273
271
 
274
- ## Complete Example: File Processing
272
+ ## Complete Example: Config Loader
275
273
 
276
- Here's a comprehensive example showing real-world usage:
274
+ A realistic example showing how to chain fallible operations:
277
275
 
278
276
  ```ts
279
- import { resultType, ResultType, chain, match } from '@iahuang/result-ts';
277
+ import { resultType, chain, match } from '@iahuang/result-ts';
280
278
  import * as fs from 'fs';
281
279
 
282
- type FileErrors = {
283
- not_found: string;
284
- permission_denied: string;
285
- invalid_json: string;
286
- };
287
-
288
- const fileResult = resultType<any, FileErrors>();
289
- type FileResult = ResultType<typeof fileResult>;
280
+ type ConfigErrors = { not_found: string; invalid_json: string };
281
+ const configResult = resultType<any, ConfigErrors>();
290
282
 
291
- function readFile(path: string): FileResult {
283
+ function loadConfig(path: string) {
284
+ // Read file
285
+ let content: string;
292
286
  try {
293
- const content = fs.readFileSync(path, 'utf-8');
294
- return fileResult.ok(content);
295
- } catch (e: any) {
296
- if (e.code === 'ENOENT') {
297
- return fileResult.err("not_found", path);
298
- }
299
- if (e.code === 'EACCES') {
300
- return fileResult.err("permission_denied", path);
301
- }
302
- throw e;
287
+ content = fs.readFileSync(path, 'utf-8');
288
+ } catch {
289
+ return configResult.err("not_found", path);
303
290
  }
304
- }
305
291
 
306
- function parseJSON(content: string): FileResult {
292
+ // Parse JSON
307
293
  try {
308
- return fileResult.ok(JSON.parse(content));
294
+ return configResult.ok(JSON.parse(content));
309
295
  } catch (e) {
310
- return fileResult.err("invalid_json", String(e));
296
+ return configResult.err("invalid_json", String(e));
311
297
  }
312
298
  }
313
299
 
314
- // Process the file with chaining
315
- const result = chain(readFile("config.json"))
316
- .andThen(parseJSON)
317
- .map((config) => config.database)
318
- .result;
319
-
320
- // Handle the result
321
- const config = match(result, {
322
- ok: (db) => db,
323
- err: (e) => {
324
- switch (e.error) {
325
- case "not_found":
326
- console.error(`File not found: ${e.detail}`);
327
- break;
328
- case "permission_denied":
329
- console.error(`Permission denied: ${e.detail}`);
330
- break;
331
- case "invalid_json":
332
- console.error(`Invalid JSON: ${e.detail}`);
333
- break;
334
- }
335
- return null;
336
- }
337
- });
300
+ // Usage
301
+ const dbHost = chain(loadConfig("config.json"))
302
+ .map((config) => config.database.host)
303
+ .unwrapOr("localhost");
338
304
  ```
339
305
 
340
306
  ## API Reference
@@ -342,8 +308,7 @@ const config = match(result, {
342
308
  ### Core Types
343
309
 
344
310
  - `Result<T, E>` - The main Result type
345
- - `Ok<T>` - Success type
346
- - `Err<E>` - Error type
311
+ - `Err<E>` - Error type (discriminated union of `{ type, detail }`)
347
312
  - `resultType<T, E>()` - Creates type-safe Result constructors
348
313
 
349
314
  ### Query Methods
@@ -379,22 +344,124 @@ const config = match(result, {
379
344
 
380
345
  ### Utilities
381
346
 
347
+ These utility functions do not have any analogous functions in Rust, but are provided for convenience.
348
+
382
349
  - `match(result, handlers)` - Pattern matching
350
+ - `matchNested(result, handlers)` - Pattern matching with per-variant error handlers
383
351
  - `chain(result)` - Method chaining API
384
- - `tryCatchResult(fn, errFn)` - Convert exceptions to Results
385
- - `tryCatchResultAsync(fn, errFn)` - Async exception handling
386
- - `tryCatchResultPromise(promise, errFn)` - Promise rejection handling
352
+ - `resultFromThrowingFunction(fn, errFn)` - Convert exceptions to Results
353
+ - `resultFromThrowingAsyncFunction(fn, errFn)` - Async exception handling
354
+ - `resultFromThrowingPromise(promise, errFn)` - Promise rejection handling
355
+ - `resultAll(results)` - Combine multiple Results into one
387
356
 
388
357
  All sync functions have async equivalents prefixed with `async`:
389
- - `asyncResultMap`, `asyncResultAndThen`, `asyncChain`, etc.
358
+ - `asyncResultMap`, `asyncResultAndThen`, `asyncChain`, `asyncResultAll`, etc.
390
359
 
391
360
  ## Comparison with Other Libraries
392
361
 
393
- ### vs. `neverthrow`
362
+ ### vs. [`neverthrow`](https://github.com/supermacro/neverthrow)
394
363
 
395
364
  Unlike `neverthrow`, this library uses plain objects instead of class instances:
396
365
  - Results can be safely serialized/deserialized (e.g., JSON.stringify)
397
- - No runtime overhead from class instantiation
398
- - More memory efficient for large datasets
399
366
  - Closer to functional programming principles
400
367
 
368
+ ### vs. [Effect](https://effect.website/)
369
+
370
+ Effect provides a much more comprehensive and opinionated ecosystem implementing functional programming principles in TypeScript. This library is focused on providing a lightweight, type-safe framework for error handling without any additional tooling, dependencies, or specific use cases in mind.
371
+
372
+ ## Migrating from v1
373
+
374
+ ### Breaking Changes
375
+
376
+ #### Result structure changed
377
+
378
+ Errors are now nested under an `error` field instead of being spread flat on the result object. The error object uses `type` instead of `error` for the variant key.
379
+
380
+ ```ts
381
+ // v1
382
+ { ok: false, error: "not_found", detail: "/path" }
383
+
384
+ // v2
385
+ { ok: false, error: { type: "not_found", detail: "/path" } }
386
+ ```
387
+
388
+ This means code that accessed `result.error` or `result.detail` directly on a failed result must now access `result.error.type` and `result.error.detail`.
389
+
390
+ #### `Ok<T>` and `Undetailed<E>` types removed
391
+
392
+ `Ok<T>` is no longer exported as a separate type. Use `Result<T, never>` if you need to represent a result that is always successful. `Undetailed<E>` has been removed entirely.
393
+
394
+ #### `ResultType<T>` helper type removed
395
+
396
+ The `ResultType<typeof myResult>` pattern for extracting a Result type from constructors is no longer supported. Use `Result<T, E>` directly instead.
397
+
398
+ ```ts
399
+ // v1
400
+ const parseResult = resultType<number, ParseErrors>();
401
+ type ParseResult = ResultType<typeof parseResult>;
402
+
403
+ // v2
404
+ const parseResult = resultType<number, ParseErrors>();
405
+ type ParseResult = Result<number, ParseErrors>;
406
+ ```
407
+
408
+ #### `resultType()` no longer returns chain constructors
409
+
410
+ The `okChain` and `errChain` constructors have been removed from the object returned by `resultType()`. Use `chain()` to wrap results instead.
411
+
412
+ ```ts
413
+ // v1
414
+ const r = myResult.okChain(value);
415
+
416
+ // v2
417
+ const r = chain(myResult.ok(value));
418
+ ```
419
+
420
+ #### `tryCatchResult` functions renamed
421
+
422
+ | v1 | v2 |
423
+ |----|-----|
424
+ | `tryCatchResult` | `resultFromThrowingFunction` |
425
+ | `tryCatchResultAsync` | `resultFromThrowingAsyncFunction` |
426
+ | `tryCatchResultPromise` | `resultFromThrowingPromise` |
427
+
428
+ #### `asyncMatch` replaced by `matchNested`
429
+
430
+ `asyncMatch` has been removed. The new `matchNested` function provides per-variant error handlers instead.
431
+
432
+ ```ts
433
+ // v1
434
+ await asyncMatch(asyncResult, {
435
+ ok: async (value) => ...,
436
+ err: (e) => ...
437
+ });
438
+
439
+ // v2
440
+ matchNested(result, {
441
+ ok: (value) => ...,
442
+ err: {
443
+ not_found: (detail) => ...,
444
+ timeout: (detail) => ...,
445
+ }
446
+ });
447
+ ```
448
+
449
+ #### `match` error handler receives `Err<E>` instead of the full result
450
+
451
+ The `err` handler in `match` now receives the `Err<E>` value (i.e. `{ type, detail }`) rather than the entire result object.
452
+
453
+ ```ts
454
+ // v1
455
+ match(result, {
456
+ ok: (v) => ...,
457
+ err: (e) => console.log(e.error, e.detail)
458
+ });
459
+
460
+ // v2
461
+ match(result, {
462
+ ok: (v) => ...,
463
+ err: (e) => console.log(e.type, e.detail)
464
+ });
465
+ ```
466
+
467
+ #### `asyncResultUnwrapOrElse` is no longer exported