@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.
- package/.claude/settings.local.json +4 -1
- package/README.md +176 -109
- package/dist/index.d.ts +72 -111
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +176 -180
- package/dist/index.js.map +1 -1
- package/index.test.ts +782 -0
- package/index.ts +134 -179
- package/package.json +6 -3
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
|
|
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;
|
|
22
|
-
negative: number;
|
|
25
|
+
invalid_number: string;
|
|
26
|
+
negative: number;
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
const parseResult = resultType<number, ParseErrors>();
|
|
26
30
|
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
```ts
|
|
42
|
+
const value = chain(parsePositiveInt("42"))
|
|
43
|
+
.map((n) => n * 2)
|
|
44
|
+
.unwrapOr(0);
|
|
68
45
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
-
|
|
58
|
+
- Success, containing a value of type `T`
|
|
79
59
|
```ts
|
|
80
60
|
{ ok: true, value: T }
|
|
81
61
|
```
|
|
82
|
-
-
|
|
62
|
+
- Failure, containing an `Err<E>` under the `error` field
|
|
83
63
|
```ts
|
|
84
|
-
{ ok: false, error:
|
|
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()`
|
|
75
|
+
#### 1. **Recommended:** Using `resultType()`
|
|
92
76
|
|
|
93
|
-
This provides
|
|
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.
|
|
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,
|
|
219
|
+
import { asyncResultMap, asyncResultAndThen, resultFromThrowingAsyncFunction } from '@iahuang/result-ts';
|
|
222
220
|
|
|
223
221
|
async function fetchUser(id: string): Promise<Result<User, FetchErrors>> {
|
|
224
|
-
return
|
|
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 {
|
|
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):
|
|
261
|
-
return
|
|
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:
|
|
272
|
+
## Complete Example: Config Loader
|
|
275
273
|
|
|
276
|
-
|
|
274
|
+
A realistic example showing how to chain fallible operations:
|
|
277
275
|
|
|
278
276
|
```ts
|
|
279
|
-
import { resultType,
|
|
277
|
+
import { resultType, chain, match } from '@iahuang/result-ts';
|
|
280
278
|
import * as fs from 'fs';
|
|
281
279
|
|
|
282
|
-
type
|
|
283
|
-
|
|
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
|
|
283
|
+
function loadConfig(path: string) {
|
|
284
|
+
// Read file
|
|
285
|
+
let content: string;
|
|
292
286
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
292
|
+
// Parse JSON
|
|
307
293
|
try {
|
|
308
|
-
return
|
|
294
|
+
return configResult.ok(JSON.parse(content));
|
|
309
295
|
} catch (e) {
|
|
310
|
-
return
|
|
296
|
+
return configResult.err("invalid_json", String(e));
|
|
311
297
|
}
|
|
312
298
|
}
|
|
313
299
|
|
|
314
|
-
//
|
|
315
|
-
const
|
|
316
|
-
.
|
|
317
|
-
.
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
385
|
-
- `
|
|
386
|
-
- `
|
|
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
|