@shirudo/result 1.1.0 → 1.1.1
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 +86 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
# @shirudo/result
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A `Result<T, E>` type for TypeScript. Functions return their failures instead of throwing them.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```ts docs-check:skip
|
|
6
|
+
function loadUser(id: string): Result<User, UserError>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The failure is part of the signature: callers see exactly what can go wrong, the compiler insists both cases are handled, and `value`/`error` are only accessible after narrowing. What used to be a forgotten `catch` is now a type error.
|
|
10
|
+
|
|
11
|
+
The rest of the package is tooling around that one type: pipe operators, async variants, generator-based do-notation, exhaustive error matching, and collection helpers. The library has zero dependencies, ships as ESM and CJS, and runs on Node 20+ and edge runtimes.
|
|
6
12
|
|
|
7
13
|
[](https://github.com/shi-rudo/result-ts/actions/workflows/ci.yml)
|
|
8
14
|
[](https://www.npmjs.com/package/@shirudo/result)
|
|
9
15
|
[](./LICENSE)
|
|
10
|
-
[](https://www.typescriptlang.org/)
|
|
11
|
-
[](https://nodejs.org/)
|
|
12
|
-

|
|
13
16
|
|
|
14
17
|
## Installation
|
|
15
18
|
|
|
@@ -22,12 +25,13 @@ yarn add @shirudo/result
|
|
|
22
25
|
## Quick Start
|
|
23
26
|
|
|
24
27
|
```ts
|
|
25
|
-
import { Result } from '@shirudo/result';
|
|
28
|
+
import { err, ok, type Result } from '@shirudo/result';
|
|
29
|
+
import { map, match } from '@shirudo/result/operators';
|
|
26
30
|
|
|
27
31
|
type User = { id: string; email: string; active: boolean };
|
|
28
32
|
type UserError =
|
|
29
|
-
| {
|
|
30
|
-
| {
|
|
33
|
+
| { code: 'not-found'; id: string }
|
|
34
|
+
| { code: 'inactive'; id: string };
|
|
31
35
|
|
|
32
36
|
const users = new Map<string, User>([
|
|
33
37
|
['1', { id: '1', email: 'ada@example.com', active: true }],
|
|
@@ -35,35 +39,53 @@ const users = new Map<string, User>([
|
|
|
35
39
|
|
|
36
40
|
function loadUser(id: string): Result<User, UserError> {
|
|
37
41
|
const user = users.get(id);
|
|
38
|
-
if (!user) return
|
|
39
|
-
if (!user.active) return
|
|
40
|
-
return
|
|
42
|
+
if (!user) return err({ code: 'not-found', id });
|
|
43
|
+
if (!user.active) return err({ code: 'inactive', id });
|
|
44
|
+
return ok(user);
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
// Narrow explicitly:
|
|
43
48
|
const result = loadUser('1');
|
|
44
|
-
|
|
45
49
|
if (result.isOk()) {
|
|
46
|
-
console.log(result.value.email);
|
|
47
|
-
} else {
|
|
48
|
-
switch (result.error.type) {
|
|
49
|
-
case 'not-found':
|
|
50
|
-
console.error(`Missing user ${result.error.id}`);
|
|
51
|
-
break;
|
|
52
|
-
case 'inactive':
|
|
53
|
-
console.error(`Inactive user ${result.error.id}`);
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
50
|
+
console.log(result.value.email); // `value` is only accessible in this branch
|
|
56
51
|
}
|
|
52
|
+
|
|
53
|
+
// Or compose and resolve in one expression. `map` only runs on Ok,
|
|
54
|
+
// and the error keeps its type all the way to `match`:
|
|
55
|
+
const message = loadUser('1').pipe(
|
|
56
|
+
map(user => `Welcome back, ${user.email}`),
|
|
57
|
+
match({
|
|
58
|
+
ok: greeting => greeting,
|
|
59
|
+
err: error =>
|
|
60
|
+
error.code === 'not-found'
|
|
61
|
+
? `No user with id ${error.id}`
|
|
62
|
+
: `User ${error.id} is deactivated`,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
57
65
|
```
|
|
58
66
|
|
|
59
|
-
## Why
|
|
67
|
+
## Why Result Instead of try/catch?
|
|
68
|
+
|
|
69
|
+
TypeScript cannot type a `catch` block: every thrown value arrives as `unknown`, and nothing in a function's signature reveals that it throws at all. Callers either remember to catch, or they find out in production.
|
|
70
|
+
|
|
71
|
+
A `Result<User, UserError>` puts the failure into the signature. The compiler forces both states to be handled, narrows `value` and `error` access to the matching state (as in the Quick Start above), and keeps the error type intact across every transformation.
|
|
72
|
+
|
|
73
|
+
## Why This Library?
|
|
74
|
+
|
|
75
|
+
There are several Result implementations for TypeScript. This one is built around a few hard guarantees:
|
|
76
|
+
|
|
77
|
+
- **Error types that cannot lie.** Declaring an explicit error type requires an error mapper: `fromPromise<User, ApiError>(promise)` without one is a compile error, so `E` never silently holds an unmapped `unknown`. And bugs inside the mapper itself are rethrown instead of being disguised as `Err` values.
|
|
78
|
+
- **Exhaustive matching, checked at compile time.** `matchError().when(NotFoundError, ...).run()` only compiles once every error case is handled, and `matchTag` does the same for discriminated unions. Add a new error variant, and every unhandled match site turns red.
|
|
79
|
+
- **Four interchangeable styles, one type.** Explicit `isOk()`/`isErr()` checks, `pipe`/`pipeAsync` operator chains, generator-based do-notation (`task`), and builder-based error matching all work on the same immutable, frozen `Result`. Use whichever style fits each call site.
|
|
80
|
+
- **Safe at runtime boundaries.** `isResult()` validates an internal brand plus payload shape instead of accepting lookalike objects, and `toSerialized()`/`fromSerialized()` round-trip Results through JSON without ambiguity.
|
|
81
|
+
- **Zero dependencies, runs anywhere.** The package ships ESM and CJS builds with tree-shakeable subpath exports, and it runs on Node 20+ and in edge runtimes.
|
|
82
|
+
- **Verified, not promised.** Every TypeScript snippet in this README and the docs is compile-checked in CI, the API is covered by 400+ runtime tests plus compile-time type tests, and the package exports are verified for ESM, CJS, and TypeScript consumers.
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
- Package exports are tested for ESM, CJS, and TypeScript consumers.
|
|
84
|
+
## When Not to Use It
|
|
85
|
+
|
|
86
|
+
- If your codebase is already built on [Effect](https://effect.website/), you do not need this package. Its `Either`/`Exit` types come with the surrounding ecosystem.
|
|
87
|
+
- For short scripts and prototypes, plain `try`/`catch` is often the simpler tool. The value of typed errors grows with the number of call sites that must handle them.
|
|
88
|
+
- Keep throwing for programmer errors. Broken invariants and failed assertions should crash loudly; `Result` is for failures the caller is expected to handle.
|
|
67
89
|
|
|
68
90
|
## Common Workflows
|
|
69
91
|
|
|
@@ -74,14 +96,14 @@ import { Result } from '@shirudo/result';
|
|
|
74
96
|
|
|
75
97
|
const parseJson = Result.fromThrowable(
|
|
76
98
|
JSON.parse,
|
|
77
|
-
error => ({
|
|
99
|
+
error => ({ code: 'parse' as const, cause: error }),
|
|
78
100
|
);
|
|
79
101
|
|
|
80
102
|
const parsed = parseJson('{"valid": true}');
|
|
81
103
|
|
|
82
104
|
const response = await Result.fromPromise(
|
|
83
105
|
Promise.resolve({ ok: true }),
|
|
84
|
-
error => ({
|
|
106
|
+
error => ({ code: 'network' as const, cause: error }),
|
|
85
107
|
);
|
|
86
108
|
```
|
|
87
109
|
|
|
@@ -131,27 +153,53 @@ function findUser(id: string) {
|
|
|
131
153
|
function ensureEmail(user: { id: string; email?: string }) {
|
|
132
154
|
return user.email
|
|
133
155
|
? Result.ok(user.email)
|
|
134
|
-
: Result.err({
|
|
156
|
+
: Result.err({ code: 'missing-email' as const, id: user.id });
|
|
135
157
|
}
|
|
136
158
|
|
|
137
|
-
const
|
|
159
|
+
const emailResult = await task(function* () {
|
|
138
160
|
const user = yield* findUser('1');
|
|
139
161
|
return yield* ensureEmail(user);
|
|
140
162
|
});
|
|
141
163
|
```
|
|
142
164
|
|
|
165
|
+
### Handle error classes exhaustively
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { Result } from '@shirudo/result';
|
|
169
|
+
|
|
170
|
+
class NotFoundError extends Error {
|
|
171
|
+
readonly code = 'not-found';
|
|
172
|
+
}
|
|
173
|
+
class RateLimitError extends Error {
|
|
174
|
+
readonly code = 'rate-limited';
|
|
175
|
+
constructor(readonly retryAfter: number) {
|
|
176
|
+
super(`rate limited, retry in ${retryAfter}s`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const result: Result<string, NotFoundError | RateLimitError> = Result.err(new RateLimitError(30));
|
|
181
|
+
|
|
182
|
+
if (result.isErr()) {
|
|
183
|
+
const message = result
|
|
184
|
+
.matchError()
|
|
185
|
+
.when(NotFoundError, () => 'No such record')
|
|
186
|
+
.when(RateLimitError, error => `Retry in ${error.retryAfter}s`)
|
|
187
|
+
.run(); // run() only compiles because every error class is handled
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
143
191
|
### Match discriminated-union errors
|
|
144
192
|
|
|
145
193
|
```ts
|
|
146
194
|
import { Result, matchTag } from '@shirudo/result';
|
|
147
195
|
|
|
148
196
|
type DomainError =
|
|
149
|
-
| {
|
|
150
|
-
| {
|
|
197
|
+
| { code: 'network'; retryAfter: number }
|
|
198
|
+
| { code: 'validation'; field: string };
|
|
151
199
|
|
|
152
|
-
const failed = Result.err<DomainError>({
|
|
200
|
+
const failed = Result.err<DomainError>({ code: 'network', retryAfter: 30 });
|
|
153
201
|
|
|
154
|
-
const message = matchTag(failed, '
|
|
202
|
+
const message = matchTag(failed, 'code', {
|
|
155
203
|
network: error => `Retry in ${error.retryAfter}s`,
|
|
156
204
|
validation: error => `Invalid field: ${error.field}`,
|
|
157
205
|
});
|
|
@@ -186,7 +234,7 @@ The full documentation lives in `docs/` and is built with VitePress.
|
|
|
186
234
|
- [Collections](docs/api/collections.md)
|
|
187
235
|
- [Error Classes](docs/api/errors.md)
|
|
188
236
|
- [Version 1 Migration](docs/migration/v1.md)
|
|
189
|
-
- [Design Decisions](docs/decisions/lazy-async-abstraction.md)
|
|
237
|
+
- [Design Decisions](docs/decisions/lazy-async-abstraction.md) ([Defensive State Checks](docs/decisions/defensive-state-checks.md))
|
|
190
238
|
|
|
191
239
|
## Development
|
|
192
240
|
|