@onrails/result 0.1.4 → 0.3.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/DESIGN.md +11 -19
- package/README.md +14 -62
- package/RECIPES.md +12 -25
- package/dist/async-BR3AX0ck.d.cts +326 -0
- package/dist/async-SZ7BEl0g.d.ts +326 -0
- package/dist/compat/neverthrow.cjs +266 -94
- package/dist/compat/neverthrow.cjs.map +1 -1
- package/dist/compat/neverthrow.d.cts +4 -4
- package/dist/compat/neverthrow.d.ts +4 -4
- package/dist/compat/neverthrow.js +263 -91
- package/dist/compat/neverthrow.js.map +1 -1
- package/dist/extra.cjs +2 -25
- package/dist/extra.cjs.map +1 -1
- package/dist/extra.d.cts +7 -19
- package/dist/extra.d.ts +7 -19
- package/dist/extra.js +3 -25
- package/dist/extra.js.map +1 -1
- package/dist/fluent.cjs +55 -29
- package/dist/fluent.cjs.map +1 -1
- package/dist/fluent.d.cts +38 -11
- package/dist/fluent.d.ts +38 -11
- package/dist/fluent.js +55 -29
- package/dist/fluent.js.map +1 -1
- package/dist/index.cjs +290 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +294 -69
- package/dist/index.d.ts +294 -69
- package/dist/index.js +287 -127
- package/dist/index.js.map +1 -1
- package/dist/pipe.cjs.map +1 -1
- package/dist/pipe.d.cts +7 -5
- package/dist/pipe.d.ts +7 -5
- package/dist/pipe.js.map +1 -1
- package/dist/railway.cjs +293 -188
- package/dist/railway.cjs.map +1 -1
- package/dist/railway.d.cts +26 -180
- package/dist/railway.d.ts +26 -180
- package/dist/railway.js +294 -178
- package/dist/railway.js.map +1 -1
- package/dist/try-gen.cjs +1 -1
- package/dist/try-gen.cjs.map +1 -1
- package/dist/try-gen.d.cts +29 -3
- package/dist/try-gen.d.ts +29 -3
- package/dist/try-gen.js +1 -1
- package/dist/try-gen.js.map +1 -1
- package/dist/types-BQ9vv0nD.d.cts +55 -0
- package/dist/types-BQ9vv0nD.d.ts +55 -0
- package/package.json +5 -18
- package/skills/railway-do-notation/SKILL.md +397 -0
- package/skills/result-composition/SKILL.md +323 -0
- package/dist/$.cjs +0 -40
- package/dist/$.cjs.map +0 -1
- package/dist/$.js +0 -36
- package/dist/$.js.map +0 -1
- package/dist/_.d.cts +0 -2
- package/dist/_.d.ts +0 -2
- package/dist/async-CH8Cs8KE.d.cts +0 -161
- package/dist/async-DKISqIBc.d.ts +0 -161
- package/dist/interop.cjs +0 -253
- package/dist/interop.cjs.map +0 -1
- package/dist/interop.d.cts +0 -35
- package/dist/interop.d.ts +0 -35
- package/dist/interop.js +0 -249
- package/dist/interop.js.map +0 -1
- package/dist/types-C2Dp1d5J.d.cts +0 -21
- package/dist/types-C2Dp1d5J.d.ts +0 -21
- package/dist/validation.cjs +0 -53
- package/dist/validation.cjs.map +0 -1
- package/dist/validation.d.cts +0 -72
- package/dist/validation.d.ts +0 -72
- package/dist/validation.js +0 -48
- package/dist/validation.js.map +0 -1
package/DESIGN.md
CHANGED
|
@@ -10,7 +10,7 @@ type Result<T, E> =
|
|
|
10
10
|
| { readonly _tag: "Err"; readonly error: E };
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
- Default API: dual-form module functions — every transform accepts either shape:
|
|
13
|
+
- Default API: dual-form module functions — every transform accepts either shape. The arity dispatch is owned by one internal combinator (`src/internal/dual.ts`, twin in `@onrails/maybe`); `test/dual.spec.ts` property-tests `f(r, x) ≡ f(x)(r)` for every transform:
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
map(result, fn); // data-first
|
|
@@ -21,11 +21,11 @@ type Result<T, E> =
|
|
|
21
21
|
|
|
22
22
|
- `flatMap` is the canonical bind and widens error types (`E | F`).
|
|
23
23
|
- `match` is the canonical terminal collapse — positional, dual-form.
|
|
24
|
-
-
|
|
24
|
+
- For files that also import `match` from `ts-pattern`, dissolve the collision with a namespace import (`import * as R from "@onrails/result"` → `R.match`).
|
|
25
25
|
- `tap` / `tapErr` observe a track without changing the carried value.
|
|
26
26
|
- `recover` binds the error track and may return a failed workflow back to success.
|
|
27
27
|
- `pipe(value, ...fns)` is the variadic value-first pipe (up to 9 steps); `flow(...fns)` is the variadic point-free composition in `@onrails/result/pipe`.
|
|
28
|
-
- Optional dot-chaining via `fluent(r)` / `fluentAsync(ra)` in `@onrails/result/fluent`.
|
|
28
|
+
- Optional dot-chaining via `fluent(r)` / `fluentAsync(ra)` in `@onrails/result/fluent`. Mirrors every instance-appropriate core transform; `test/parity.spec.ts` enforces the mirror (both directions) across core, fluent, and the neverthrow compat shim.
|
|
29
29
|
|
|
30
30
|
## Sync / async interop
|
|
31
31
|
|
|
@@ -62,24 +62,18 @@ type RailwayOutput<T, E, M extends RailwayMode> =
|
|
|
62
62
|
M extends "async" ? ResultAsync<T, E> : Result<T, E>;
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
Use `Railway` when named context removes nesting or positional tuple plumbing. Prefer `asyncAfter`, `fromResult`, or direct `flatMap` for one- or two-step flows.
|
|
65
|
+
Use `Railway` when named context removes nesting or positional tuple plumbing. Prefer `asyncAfter`, `fromResult`, or direct `flatMap` for one- or two-step flows. Reusable steps are plain functions of the context (`(ctx) => Result` / `(ctx) => ResultAsync`) plugged in via `.fromResult` / `.fromAsync`.
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
Internally the runtime state tag (`"sync" | "async"`) mirrors the phantom `M`; the private `step`/`out` helpers are the only places that re-link the two.
|
|
68
68
|
|
|
69
69
|
## Combining results
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
- `sequenceTupleAsync` — async, sequential, first-Err in input order.
|
|
73
|
-
- `parallelTupleAsync` — async, concurrent (branches overlap), first-Err in input order.
|
|
74
|
-
- `ResultAsync.combine` — homogeneous async collection.
|
|
75
|
-
|
|
76
|
-
## Validation
|
|
71
|
+
One module (`collections.ts`), one naming matrix — short-circuit vs accumulate × array vs tuple:
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
- `
|
|
81
|
-
- `
|
|
82
|
-
- `validateTupleArray(results)` — same for heterogeneous tuple shapes.
|
|
73
|
+
- `combine` / `combineTuple` — sync, first-Err wins.
|
|
74
|
+
- `validateAll` / `validateTuple` — accumulate independent failures; optional `combineErrors` fold, default collects a `readonly E[]`.
|
|
75
|
+
- `ResultAsync.combine` / `ResultAsync.combineTuple` — async, sequential, first-Err in input order.
|
|
76
|
+
- `ResultAsync.combineTupleParallel` — async, concurrent (branches overlap), first-Err in input order.
|
|
83
77
|
|
|
84
78
|
Use `flatMap` for dependent checks where later checks need earlier successful values.
|
|
85
79
|
|
|
@@ -95,11 +89,9 @@ Use `flatMap` for dependent checks where later checks need earlier successful va
|
|
|
95
89
|
| `@onrails/result` | Core: dual-form map/flatMap/match, variadic `pipe`, sync collection, async surface, lift helpers, generator sugar |
|
|
96
90
|
| `@onrails/result/fluent` | Dot-style chains |
|
|
97
91
|
| `@onrails/result/extra` | Error-type utilities |
|
|
98
|
-
| `@onrails/result/interop` | Promise/ResultAsync boundary helpers |
|
|
99
92
|
| `@onrails/result/pipe` | Variadic `flow` for point-free composition |
|
|
100
|
-
| `@onrails/result/railway` | Named workflow builder
|
|
93
|
+
| `@onrails/result/railway` | Named workflow builder |
|
|
101
94
|
| `@onrails/result/try-gen` | Sync generator-style `Result` sugar |
|
|
102
|
-
| `@onrails/result/validation` | Independent validation with accumulated failures |
|
|
103
95
|
| `@onrails/result/compat/neverthrow` | Migration shim — class-shaped surface for incremental migration off neverthrow |
|
|
104
96
|
|
|
105
97
|
## Type tests
|
package/README.md
CHANGED
|
@@ -11,26 +11,17 @@ bun add @onrails/result
|
|
|
11
11
|
## Quick start (value-first — best inference)
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import {
|
|
15
|
-
asyncAfter,
|
|
16
|
-
err,
|
|
17
|
-
flatMapResult,
|
|
18
|
-
fromAsync,
|
|
19
|
-
mapResult,
|
|
20
|
-
match,
|
|
21
|
-
ok,
|
|
22
|
-
trySync,
|
|
23
|
-
} from "@onrails/result";
|
|
14
|
+
import { err, flatMap, match, ok, trySync } from "@onrails/result";
|
|
24
15
|
|
|
25
16
|
const parse = trySync(
|
|
26
17
|
(raw: string) => JSON.parse(raw),
|
|
27
18
|
(e) => ({ kind: "parse" as const, message: String(e) }),
|
|
28
19
|
);
|
|
29
20
|
|
|
30
|
-
const pipeline =
|
|
21
|
+
const pipeline = flatMap(parse('{"v":1}'), (data) => ok(data.v));
|
|
31
22
|
```
|
|
32
23
|
|
|
33
|
-
|
|
24
|
+
Every transform is dual-form: data-first `flatMap(r, fn)` (best inference) or curried `flatMap(fn)(r)` for `pipe`/`flow`. Long chains: `fluent()` from `@onrails/result/fluent`.
|
|
34
25
|
|
|
35
26
|
For worked examples of multi-step pipelines, parser builders, validator ladders, and parallel sub-workflows see [RECIPES.md](./RECIPES.md).
|
|
36
27
|
|
|
@@ -43,9 +34,9 @@ For worked examples of multi-step pipelines, parser builders, validator ladders,
|
|
|
43
34
|
| Long sync chain, value-first | `pipe(r, map(...), flatMap(...), ...)` |
|
|
44
35
|
| Long sync chain, dot-style preferred | `fluent(r)` from `@onrails/result/fluent` |
|
|
45
36
|
| Reusable composed function | `flow(...)` from `@onrails/result/pipe` |
|
|
46
|
-
| Several named sync/async steps | `Railway.*`
|
|
37
|
+
| Several named sync/async steps | `Railway.*` builder from `@onrails/result/railway` |
|
|
47
38
|
| Linear sync with early-return feel | `tryGen` + `$` from `@onrails/result/try-gen` |
|
|
48
|
-
| Independent validations, accumulated failures | `validateAll` / `validateTuple` from `@onrails/result
|
|
39
|
+
| Independent validations, accumulated failures | `validateAll` / `validateTuple` from `@onrails/result` |
|
|
49
40
|
| Sync → async lift, keep error type | `fromResult`, `asyncAfter` (do **not** use `fromAsync` here) |
|
|
50
41
|
| `Promise<Result<…>>` boundary lift | `fromAsync` / `tryAsync` |
|
|
51
42
|
|
|
@@ -141,9 +132,6 @@ import { match } from "ts-pattern";
|
|
|
141
132
|
R.match(result, onOk, onErr);
|
|
142
133
|
```
|
|
143
134
|
|
|
144
|
-
> [!NOTE]
|
|
145
|
-
> `matchResult` is deprecated and will be removed in the next major version.
|
|
146
|
-
|
|
147
135
|
`unwrapOk` and `unwrapErr` are test/assertion helpers. Prefer `match`, `isOk`, or `isErr` in production control flow.
|
|
148
136
|
|
|
149
137
|
```ts
|
|
@@ -167,7 +155,7 @@ const out = tryGen(() => {
|
|
|
167
155
|
});
|
|
168
156
|
```
|
|
169
157
|
|
|
170
|
-
Use `ResultAsync.combineTuple` (or `
|
|
158
|
+
Use `ResultAsync.combineTuple` (or `ResultAsync.combineTupleParallel` when branches should overlap) when combining heterogeneous async results and destructuring the result:
|
|
171
159
|
|
|
172
160
|
```ts
|
|
173
161
|
import { ResultAsync } from "@onrails/result";
|
|
@@ -209,53 +197,18 @@ Sync-only workflows return `Result<T, E>`. The first `fromPromise`, `fromAsync`,
|
|
|
209
197
|
|
|
210
198
|
Use lower-level helpers (`asyncAfter`, `fromResult`, `flatMapResult`) for one or two steps where a builder would add ceremony.
|
|
211
199
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
Use lowercase `railway(...)` when the steps should be named once and reused across workflows:
|
|
200
|
+
To share steps across workflows, extract plain functions of the context and plug them in via `.fromResult` / `.fromAsync`:
|
|
215
201
|
|
|
216
202
|
```ts
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
fromPromiseNamed,
|
|
220
|
-
parallelNamed,
|
|
221
|
-
parseWith,
|
|
222
|
-
railway,
|
|
223
|
-
requireNamed,
|
|
224
|
-
select,
|
|
225
|
-
} from "@onrails/result/railway";
|
|
226
|
-
|
|
227
|
-
const parseProfileId = parseWith(ProfileIdSchema, toError).as("profileId");
|
|
228
|
-
|
|
229
|
-
const loadProfileRow = fromPromiseNamed(
|
|
230
|
-
"row",
|
|
231
|
-
({ profileId }) => loadProfileRowById(profileId),
|
|
232
|
-
toError,
|
|
233
|
-
);
|
|
203
|
+
const loadProfileRow = ({ profileId }: { profileId: string }) =>
|
|
204
|
+
tryAsync(loadProfileRowById(profileId), toError);
|
|
234
205
|
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
const loadSummaryInputs = parallelNamed({
|
|
240
|
-
recentArtifacts: ({ profile }) => loadRecentArtifacts(profile.id),
|
|
241
|
-
jobMetrics: ({ profile }) => loadJobMetrics(profile.id),
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const summary = railway(
|
|
245
|
-
id,
|
|
246
|
-
parseProfileId,
|
|
247
|
-
loadProfileRow,
|
|
248
|
-
requireProfile,
|
|
249
|
-
deriveNamed("normalized", ({ profile }) => normalizeProfile(profile)),
|
|
250
|
-
loadSummaryInputs,
|
|
251
|
-
select(({ normalized, recentArtifacts, jobMetrics }) =>
|
|
252
|
-
toProfileSummary({ normalized, recentArtifacts, jobMetrics }),
|
|
253
|
-
),
|
|
254
|
-
);
|
|
206
|
+
const summary = Railway.fromSync("profileId", () => ProfileIdSchema.parse(id), toError)
|
|
207
|
+
.fromAsync("row", loadProfileRow)
|
|
208
|
+
.require("profile", "row", ({ profileId }) => new Error(`Profile not found: ${profileId}`))
|
|
209
|
+
.select(({ profile }) => toProfileSummary(profile));
|
|
255
210
|
```
|
|
256
211
|
|
|
257
|
-
`railway(input, ...steps)` starts from `{ input }`. `parseWith(...).as(key)` is the usual first step for raw input. The final output is still mode-aware: sync-only steps return `Result`, while async steps return `ResultAsync`.
|
|
258
|
-
|
|
259
212
|
## Pipe
|
|
260
213
|
|
|
261
214
|
```ts
|
|
@@ -307,9 +260,8 @@ import { ResultAsync, Result, ok, err, okAsync, errAsync } from "@onrails/result
|
|
|
307
260
|
| `@onrails/result` | Core + interop exports |
|
|
308
261
|
| `@onrails/result/fluent` | `fluent()`, `fluentAsync()` |
|
|
309
262
|
| `@onrails/result/extra` | Error-type utilities |
|
|
310
|
-
| `@onrails/result/interop` | `fromAsync`, `fromResult`, `asyncAfter` |
|
|
311
263
|
| `@onrails/result/pipe` | `flow` (variadic point-free composition) |
|
|
312
|
-
| `@onrails/result/railway` | `Railway
|
|
264
|
+
| `@onrails/result/railway` | `Railway` named-context workflow builder |
|
|
313
265
|
| `@onrails/result/try-gen` | `tryGen`, `yieldResult`, `$` |
|
|
314
266
|
| `@onrails/result/compat/neverthrow` | Migration shim |
|
|
315
267
|
|
package/RECIPES.md
CHANGED
|
@@ -20,7 +20,7 @@ To keep codebases readable and consistent, follow the four-tier decision guideli
|
|
|
20
20
|
| **1** | 1–2 steps, linear | direct data-first calls or method chains |
|
|
21
21
|
| **2** | 3+ steps, linear | `pipe` or `flow` |
|
|
22
22
|
| **3** | branchy, value reused | `tryGen` escape hatch |
|
|
23
|
-
| **4** | 4+ named steps, mixed IO | `Railway`
|
|
23
|
+
| **4** | 4+ named steps, mixed IO | `Railway` builder |
|
|
24
24
|
|
|
25
25
|
`/fluent` is documented as app-edge sugar only — never in library or service internals.
|
|
26
26
|
|
|
@@ -197,14 +197,14 @@ const requireOrder = requireRow(orderCache.get);
|
|
|
197
197
|
Three independent async loads overlap in wall-clock time; downstream `.map` reshapes the tuple.
|
|
198
198
|
|
|
199
199
|
```ts
|
|
200
|
-
import { pipe,
|
|
200
|
+
import { pipe, ResultAsync } from "@onrails/result";
|
|
201
201
|
|
|
202
202
|
type ProfileError = { kind: "profile"; cause: unknown };
|
|
203
203
|
type MetricsError = { kind: "metrics"; cause: unknown };
|
|
204
204
|
|
|
205
205
|
const buildSummary = (userId: string) =>
|
|
206
206
|
pipe(
|
|
207
|
-
|
|
207
|
+
ResultAsync.combineTupleParallel([
|
|
208
208
|
loadProfile(userId), // ResultAsync<Profile, ProfileError>
|
|
209
209
|
loadRecentMetrics(userId), // ResultAsync<Metrics, MetricsError>
|
|
210
210
|
loadFeatureFlags(userId), // ResultAsync<Flags, never>
|
|
@@ -220,7 +220,7 @@ const buildSummary = (userId: string) =>
|
|
|
220
220
|
// ResultAsync<Summary, ProfileError | MetricsError>
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
`
|
|
223
|
+
`ResultAsync.combineTupleParallel` preserves tuple positions, so destructuring stays type-safe. Use static `ResultAsync.combineTuple` if branches must run left-to-right.
|
|
224
224
|
|
|
225
225
|
---
|
|
226
226
|
|
|
@@ -435,41 +435,28 @@ The middle step can't go point-free: `profile` is reused across both the `flatMa
|
|
|
435
435
|
|
|
436
436
|
---
|
|
437
437
|
|
|
438
|
-
## 13.
|
|
438
|
+
## 13. Railway workflow pipelines (named-context builder)
|
|
439
439
|
|
|
440
|
-
Use
|
|
440
|
+
Use the `Railway` builder from `@onrails/result/railway` to build a multi-step async pipeline with named context fields. If any step is async, the entire pipeline resolves to a `ResultAsync`.
|
|
441
441
|
|
|
442
442
|
```ts
|
|
443
|
-
import {
|
|
443
|
+
import { Railway } from "@onrails/result/railway";
|
|
444
444
|
import { type ResultAsync } from "@onrails/result";
|
|
445
445
|
|
|
446
446
|
type Dashboard = { title: string };
|
|
447
447
|
|
|
448
|
-
type IdContext = { readonly id: string };
|
|
449
|
-
type ProfileContext = { readonly profile: Profile };
|
|
450
|
-
type TitleContext = { readonly title: string };
|
|
451
|
-
|
|
452
448
|
declare const IdSchema: { parse: (x: unknown) => string };
|
|
453
449
|
declare const fetchProfile: (id: string) => Promise<Profile>;
|
|
454
450
|
declare const toError: (e: unknown) => Error;
|
|
455
451
|
|
|
456
452
|
const loadDashboard = (rawId: unknown): ResultAsync<Dashboard, Error> =>
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
"profile",
|
|
462
|
-
({ id }: IdContext) => fetchProfile(id),
|
|
463
|
-
toError,
|
|
464
|
-
),
|
|
465
|
-
deriveNamed("title", ({ profile }: ProfileContext) =>
|
|
466
|
-
profile.name.toUpperCase(),
|
|
467
|
-
),
|
|
468
|
-
select(({ title }: TitleContext) => ({ title })),
|
|
469
|
-
);
|
|
453
|
+
Railway.fromSync("id", () => IdSchema.parse(rawId), toError)
|
|
454
|
+
.fromPromise("profile", ({ id }) => fetchProfile(id), toError)
|
|
455
|
+
.derive("title", ({ profile }) => profile.name.toUpperCase())
|
|
456
|
+
.select(({ title }) => ({ title }));
|
|
470
457
|
```
|
|
471
458
|
|
|
472
|
-
Every step
|
|
459
|
+
Every step reads name-first (`fromSync("id", …)`, `fromPromise("profile", …)`, `derive("title", …)`) so the field names line up at the left edge, and each step's callback sees the fully-typed context accumulated so far — no manual context annotations needed.
|
|
473
460
|
|
|
474
461
|
---
|
|
475
462
|
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { R as Result, U as UnexpectedError } from './types-BQ9vv0nD.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @onrails/result internal — the single payload-extraction pair.
|
|
5
|
+
*
|
|
6
|
+
* `InferOk` / `InferErr` pull the success/error type out of a {@link Result}
|
|
7
|
+
* (tagged union) or a {@link ResultAsync} (class carrier), resolving to
|
|
8
|
+
* `never` for anything else. Every module that needs to extract a payload
|
|
9
|
+
* type imports from here; do not redeclare local copies.
|
|
10
|
+
*
|
|
11
|
+
* Internal only: not listed in package.json `exports` or tsup entries.
|
|
12
|
+
* The package index re-exports the pair under the same names.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extracts the `Ok` value type from a {@link Result} or {@link ResultAsync},
|
|
17
|
+
* resolving to `never` for any other type.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* type T = InferOk<Result<number, string>>; // number
|
|
22
|
+
* type U = InferOk<ResultAsync<User, AppError>>; // User
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
type InferOk<R> = R extends {
|
|
26
|
+
_tag: "Ok";
|
|
27
|
+
readonly value: infer T;
|
|
28
|
+
} ? T : R extends ResultAsync<infer T, unknown> ? T : never;
|
|
29
|
+
/**
|
|
30
|
+
* Extracts the `Err` error type from a {@link Result} or {@link ResultAsync},
|
|
31
|
+
* resolving to `never` for any other type. Mirror of {@link InferOk}.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* type E = InferErr<Result<number, string>>; // string
|
|
36
|
+
* type F = InferErr<ResultAsync<User, AppError>>; // AppError
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
type InferErr<R> = R extends {
|
|
40
|
+
_tag: "Err";
|
|
41
|
+
readonly error: infer E;
|
|
42
|
+
} ? E : R extends ResultAsync<unknown, infer E> ? E : never;
|
|
43
|
+
|
|
44
|
+
type PromiseFactory<T, E> = () => Promise<Result<T, E>>;
|
|
45
|
+
type CombineTupleAsync<R extends readonly ResultAsync<unknown, unknown>[]> = ResultAsync<{
|
|
46
|
+
[K in keyof R]: InferOk<R[K]>;
|
|
47
|
+
}, {
|
|
48
|
+
[K in keyof R]: InferErr<R[K]>;
|
|
49
|
+
}[number]>;
|
|
50
|
+
/**
|
|
51
|
+
* Async railway carrier. Wraps a deferred `Promise<Result<T, E>>` and exposes
|
|
52
|
+
* the same dual-track transforms as the sync {@link Result} — `map`, `flatMap`,
|
|
53
|
+
* `recover`, `tap`, `match`. The public API never surfaces `Promise<Result<…>>`
|
|
54
|
+
* directly: `await` the instance (it is thenable) or call {@link resolve} /
|
|
55
|
+
* {@link match} to settle it.
|
|
56
|
+
*
|
|
57
|
+
* Construct via the static factories ({@link ok}, {@link err}, {@link fromPromise},
|
|
58
|
+
* {@link defer}, …) rather than `new` — the constructor is `protected`.
|
|
59
|
+
*
|
|
60
|
+
* @typeParam T - the `Ok` value type
|
|
61
|
+
* @typeParam E - the `Err` error type
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const user = ResultAsync.fromPromise(api.getUser(id), toAppError)
|
|
66
|
+
* .map((u) => u.profile)
|
|
67
|
+
* .recover(() => ResultAsync.ok(guestProfile));
|
|
68
|
+
*
|
|
69
|
+
* const r = await user; // Result<Profile, AppError>
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare class ResultAsync<T, E> {
|
|
73
|
+
protected readonly run: PromiseFactory<T, E>;
|
|
74
|
+
private promise;
|
|
75
|
+
protected constructor(run: PromiseFactory<T, E>);
|
|
76
|
+
/**
|
|
77
|
+
* Lifts an already-settled sync {@link Result} into a {@link ResultAsync}.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* const ra = ResultAsync.fromResult(ok(42)); // ResultAsync<number, never>
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
static fromResult<T, E>(result: Result<T, E>): ResultAsync<T, E>;
|
|
85
|
+
/**
|
|
86
|
+
* Wraps a `PromiseLike<T>` that may reject. Rejections pass through `onReject`
|
|
87
|
+
* to become a typed `Err`; resolution becomes `Ok<T>`.
|
|
88
|
+
*
|
|
89
|
+
* @param promise - the promise to wrap
|
|
90
|
+
* @param onReject - maps a rejection reason to the `Err` channel
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* const body = ResultAsync.fromPromise(
|
|
95
|
+
* fetch(url).then((r) => r.text()),
|
|
96
|
+
* (e): NetError => ({ kind: "net", cause: String(e) }),
|
|
97
|
+
* );
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
static fromPromise<T, E>(promise: PromiseLike<T>, onReject: (error: unknown) => E): ResultAsync<T, E>;
|
|
101
|
+
/**
|
|
102
|
+
* Wraps a `PromiseLike<T>` that is **guaranteed not to reject**, skipping the
|
|
103
|
+
* `onReject` mapper. Use only when rejection is provably impossible.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const now = ResultAsync.fromSafePromise(Promise.resolve(Date.now()));
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
static fromSafePromise<T, E = never>(promise: PromiseLike<T>): ResultAsync<T, E>;
|
|
111
|
+
/**
|
|
112
|
+
* Defers work until {@link resolve}. Unlike {@link fromPromise}, the factory
|
|
113
|
+
* does not run until the `ResultAsync` is resolved (e.g. by `combineTuple` /
|
|
114
|
+
* `combineTupleParallel`). The factory runs at most once — `resolve` memoizes.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* const insert = ResultAsync.defer(() => db.orders.insert(row));
|
|
119
|
+
* // nothing has run yet
|
|
120
|
+
* const r = await insert; // factory runs exactly once here
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
static defer<T, E>(fn: () => Promise<Result<T, E>>): ResultAsync<T, E>;
|
|
124
|
+
/**
|
|
125
|
+
* Lifts a value into an `Ok` async result.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* const r = ResultAsync.ok(42); // ResultAsync<number, never>
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
static ok<T>(value: T): ResultAsync<T, never>;
|
|
133
|
+
static ok<T, E>(value: T): ResultAsync<T, E>;
|
|
134
|
+
/**
|
|
135
|
+
* Lifts an error into an `Err` async result.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* const r = ResultAsync.err({ kind: "not_found" as const });
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
static err<T = never, E = unknown>(error: E): ResultAsync<T, E>;
|
|
143
|
+
/**
|
|
144
|
+
* Lifts an existing `Promise<Result<T, E>>` (e.g. from interop code) into a
|
|
145
|
+
* {@link ResultAsync}. A thrown defect is routed through `onDefect`,
|
|
146
|
+
* defaulting to {@link UnexpectedError}. The `Ok` value passes through
|
|
147
|
+
* verbatim — even when it happens to be Result-shaped.
|
|
148
|
+
*
|
|
149
|
+
* @param promise - a promise that already yields a `Result`
|
|
150
|
+
* @param onDefect - maps an unexpected throw to the `Err` channel
|
|
151
|
+
* @see {@link fromAsync} from `@onrails/result`
|
|
152
|
+
*/
|
|
153
|
+
static fromResultPromise<T, E>(promise: Promise<Result<T, E>>, onDefect?: (error: unknown) => E | UnexpectedError): ResultAsync<T, E | UnexpectedError>;
|
|
154
|
+
/**
|
|
155
|
+
* Combines a homogeneous array of async results into one. Resolves
|
|
156
|
+
* **sequentially** in input order, short-circuiting on the first `Err`.
|
|
157
|
+
* For heterogeneous tuples that preserve per-index types, use
|
|
158
|
+
* {@link combineTuple}; for wall-clock overlap, {@link combineTupleParallel}.
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* const all = ResultAsync.combine([loadA(), loadB(), loadC()]);
|
|
163
|
+
* // ResultAsync<Item[], LoadError>
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
static combine<T, E>(results: readonly ResultAsync<T, E>[]): ResultAsync<T[], E>;
|
|
167
|
+
/**
|
|
168
|
+
* Heterogeneous async tuple combine — resolves branches **sequentially**
|
|
169
|
+
* (left-to-right), returning the first `Err` in input order. Preserves each
|
|
170
|
+
* branch's `Ok` type by position, so the result destructures type-safely.
|
|
171
|
+
* This is the canonical sequential async combine (replaces the former
|
|
172
|
+
* `sequenceTupleAsync`).
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* const combined = ResultAsync.combineTuple([loadCfg(), loadCatalog()] as const);
|
|
177
|
+
* // ResultAsync<readonly [Cfg, Catalog], CfgError | CatalogError>
|
|
178
|
+
* const r = await combined;
|
|
179
|
+
* if (isOk(r)) {
|
|
180
|
+
* const [cfg, catalog] = r.value; // typed per position
|
|
181
|
+
* }
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
static combineTuple<const R extends readonly ResultAsync<unknown, unknown>[]>(results: R): CombineTupleAsync<R>;
|
|
185
|
+
/**
|
|
186
|
+
* Like {@link combineTuple}, but starts every branch before awaiting (wall-clock
|
|
187
|
+
* parallel for independent IO). On failure, returns the first `Err` in input order.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* // independent IO — overlap them
|
|
192
|
+
* const combined = ResultAsync.combineTupleParallel([
|
|
193
|
+
* loadProfile(id),
|
|
194
|
+
* loadMetrics(id),
|
|
195
|
+
* ] as const);
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
static combineTupleParallel<const R extends readonly ResultAsync<unknown, unknown>[]>(results: R): CombineTupleAsync<R>;
|
|
199
|
+
/**
|
|
200
|
+
* Transforms the `Ok` value, passing `Err` through unchanged.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* ResultAsync.ok(2).map((n) => n * 3); // ResultAsync<number> → Ok 6
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
map<U>(fn: (value: T) => U): ResultAsync<U, E>;
|
|
208
|
+
/**
|
|
209
|
+
* Transforms the `Err` value, passing `Ok` through unchanged — useful for
|
|
210
|
+
* unifying heterogeneous failures into one app-level union.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* load(id).mapErr((e): AppError => ({ kind: "load", cause: e })); // ResultAsync<T, AppError>
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
mapErr<F>(fn: (error: E) => F): ResultAsync<T, F>;
|
|
218
|
+
/**
|
|
219
|
+
* Canonical bind — chains a step that itself returns a `ResultAsync` or sync
|
|
220
|
+
* `Result`, short-circuiting on `Err`. Error types accumulate (`E | F`).
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* authenticate(req)
|
|
225
|
+
* .flatMap((user) => loadProfile(user.id)) // ResultAsync
|
|
226
|
+
* .flatMap((p) => validate(p)); // sync Result also accepted
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
flatMap<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F>): ResultAsync<U, E | F>;
|
|
230
|
+
/**
|
|
231
|
+
* neverthrow-compat alias of {@link flatMap}. Kept as the documented compat
|
|
232
|
+
* tier; prefer {@link flatMap} in new code.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* authenticate(req).andThen((user) => loadProfile(user.id));
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
andThen<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F>): ResultAsync<U, E | F>;
|
|
240
|
+
/**
|
|
241
|
+
* Error-channel bind — runs `fn` only on `Err`, swapping in a recovery
|
|
242
|
+
* `ResultAsync` or sync `Result`. `Ok` passes through. The mirror of
|
|
243
|
+
* {@link flatMap} on the error track.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* loadFromCache(id).recover(() => loadFromOrigin(id)); // ResultAsync<T, F>
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
recover<F>(fn: (error: E) => ResultAsync<T, F> | Result<T, F>): ResultAsync<T, F>;
|
|
251
|
+
/**
|
|
252
|
+
* neverthrow-compat alias of {@link recover}. Kept as the documented compat
|
|
253
|
+
* tier; prefer {@link recover} in new code.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* loadFromCache(id).orElse(() => loadFromOrigin(id));
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
orElse<F>(fn: (error: E) => ResultAsync<T, F> | Result<T, F>): ResultAsync<T, F>;
|
|
261
|
+
/**
|
|
262
|
+
* Runs a side effect on the `Ok` value and passes the result through
|
|
263
|
+
* unchanged; a no-op on `Err`. Mirrors {@link tapErr}.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* saveUser(u).tap((saved) => analytics.track("user_saved", saved.id));
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
tap(fn: (value: T) => void): ResultAsync<T, E>;
|
|
271
|
+
/**
|
|
272
|
+
* Runs a side effect on the `Err` value and passes the result through
|
|
273
|
+
* unchanged; a no-op on `Ok`. The error-track mirror of {@link tap}.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* saveUser(u).tapErr((e) => logger.warn("save failed", e));
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
tapErr(fn: (error: E) => void): ResultAsync<T, E>;
|
|
281
|
+
/**
|
|
282
|
+
* Settles to the `Ok` value, or `defaultValue` if `Err`. Terminal — returns a
|
|
283
|
+
* plain `Promise`, not a `ResultAsync`.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```ts
|
|
287
|
+
* const profile = await loadProfile(id).unwrapOr(guestProfile);
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
290
|
+
unwrapOr<U>(defaultValue: U): Promise<T | U>;
|
|
291
|
+
/**
|
|
292
|
+
* Terminal collapse — folds both tracks into a single awaited value. Returns
|
|
293
|
+
* a plain `Promise`, settling the carrier exactly once.
|
|
294
|
+
*
|
|
295
|
+
* @param onOk - handles the `Ok` value
|
|
296
|
+
* @param onErr - handles the `Err` value
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* const status = await save(row).match(
|
|
301
|
+
* () => 200,
|
|
302
|
+
* (e) => (e.kind === "conflict" ? 409 : 500),
|
|
303
|
+
* );
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
match<U1, U2 = U1>(onOk: (value: T) => U1, onErr: (error: E) => U2): Promise<U1 | U2>;
|
|
307
|
+
/**
|
|
308
|
+
* Settles the carrier to a bare sync {@link Result}, memoizing so the
|
|
309
|
+
* underlying factory runs at most once. Prefer `await ra` (the thenable) or
|
|
310
|
+
* {@link match} in app code; use `resolve` when you need the tagged union back.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```ts
|
|
314
|
+
* const r = await load(id).resolve(); // Result<Data, LoadError>
|
|
315
|
+
* if (isOk(r)) use(r.value);
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
resolve(): Promise<Result<T, E>>;
|
|
319
|
+
/**
|
|
320
|
+
* Thenable shim — `await ra` resolves to a bare tagged-union `Result<T, E>`.
|
|
321
|
+
* Narrow with `isOk(r)` / `isErr(r)` to read `.value` / `.error`.
|
|
322
|
+
*/
|
|
323
|
+
then<R1 = Result<T, E>, R2 = never>(onfulfilled?: ((value: Result<T, E>) => R1 | PromiseLike<R1>) | undefined | null, onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | undefined | null): Promise<R1 | R2>;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export { type InferErr as I, ResultAsync as R, type InferOk as a };
|