@onrails/result 0.3.1 → 0.4.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/DESIGN.md +4 -3
- package/README.md +166 -13
- package/RECIPES.md +6 -4
- package/dist/async-fns.cjs +52 -0
- package/dist/async-fns.cjs.map +1 -0
- package/dist/async-fns.d.cts +143 -0
- package/dist/async-fns.d.ts +143 -0
- package/dist/async-fns.js +42 -0
- package/dist/async-fns.js.map +1 -0
- package/dist/async-lift.d.cts +109 -0
- package/dist/async-lift.d.ts +109 -0
- package/dist/{async-Ci6WSL5-.d.ts → async.d.cts} +42 -58
- package/dist/{async-DcXR7k9K.d.cts → async.d.ts} +42 -58
- package/dist/collections.d.cts +105 -0
- package/dist/collections.d.ts +105 -0
- package/dist/compat/neverthrow.cjs +57 -45
- package/dist/compat/neverthrow.cjs.map +1 -1
- package/dist/compat/neverthrow.d.cts +21 -24
- package/dist/compat/neverthrow.d.ts +21 -24
- package/dist/compat/neverthrow.js +56 -44
- package/dist/compat/neverthrow.js.map +1 -1
- package/dist/extra.cjs +3 -1
- package/dist/extra.cjs.map +1 -1
- package/dist/extra.d.cts +11 -14
- package/dist/extra.d.ts +11 -14
- package/dist/extra.js +3 -1
- package/dist/extra.js.map +1 -1
- package/dist/fluent.cjs +15 -28
- package/dist/fluent.cjs.map +1 -1
- package/dist/fluent.d.cts +4 -34
- package/dist/fluent.d.ts +4 -34
- package/dist/fluent.js +16 -28
- package/dist/fluent.js.map +1 -1
- package/dist/index.cjs +77 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -490
- package/dist/index.d.ts +28 -490
- package/dist/index.js +77 -53
- package/dist/index.js.map +1 -1
- package/dist/internal/ctor.d.cts +12 -0
- package/dist/internal/ctor.d.ts +12 -0
- package/dist/internal/dual.d.cts +42 -0
- package/dist/internal/dual.d.ts +42 -0
- package/dist/internal/infer.d.cts +40 -0
- package/dist/internal/infer.d.ts +40 -0
- package/dist/internal/lock.d.cts +16 -0
- package/dist/internal/lock.d.ts +16 -0
- package/dist/internal/to-error.d.cts +2 -0
- package/dist/internal/to-error.d.ts +2 -0
- package/dist/pipe.cjs +8 -0
- package/dist/pipe.cjs.map +1 -1
- package/dist/pipe.d.cts +58 -11
- package/dist/pipe.d.ts +58 -11
- package/dist/pipe.js +8 -1
- package/dist/pipe.js.map +1 -1
- package/dist/railway.cjs +87 -63
- package/dist/railway.cjs.map +1 -1
- package/dist/railway.d.cts +49 -19
- package/dist/railway.d.ts +49 -19
- package/dist/railway.js +87 -63
- package/dist/railway.js.map +1 -1
- package/dist/result.d.cts +315 -0
- package/dist/result.d.ts +315 -0
- package/dist/try-gen.cjs +3 -3
- package/dist/try-gen.cjs.map +1 -1
- package/dist/try-gen.d.cts +4 -7
- package/dist/try-gen.d.ts +4 -7
- package/dist/try-gen.js +3 -3
- package/dist/try-gen.js.map +1 -1
- package/dist/{types-BQ9vv0nD.d.cts → types.d.cts} +6 -8
- package/dist/{types-BQ9vv0nD.d.ts → types.d.ts} +6 -8
- package/package.json +69 -24
- package/skills/railway-do-notation/SKILL.md +9 -9
package/DESIGN.md
CHANGED
|
@@ -21,12 +21,13 @@ 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
|
+
- House style: `import * as R from "@onrails/result"`. `R.` autocompletes the full surface (data-first's one ergonomic debit) and sidesteps name collisions — most commonly `match` from `ts-pattern` or `@onrails/pattern` (`R.match` next to `P.matchTag`).
|
|
25
25
|
- `of` is the Fantasy Land `pure` alias of `ok`, mirrored by `ResultAsync.of` (`Maybe.of` follows in RFC 0002); `show(r)` prints `Ok(…)` / `Err(…)` for logs (fluent's `toString` delegates to it).
|
|
26
26
|
- `tap` / `tapErr` observe a track without changing the carried value.
|
|
27
27
|
- `recover` binds the error track and may return a failed workflow back to success.
|
|
28
28
|
- `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`.
|
|
29
|
-
- Optional dot-chaining via `fluent(r)`
|
|
29
|
+
- Optional dot-chaining via `fluent(r)` 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. `ResultAsync` is already fluent (its methods return `ResultAsync`) — no `fluentAsync` wrapper; wrapping it would just be a one-line delegate to itself.
|
|
30
|
+
- The fluent wrapper stays local: `fluent(r)` opens and a terminal (`toResult`/`toString`/`match`/`unwrapOr`) closes within one expression — it's a closure over data, not data, so it must never be a return type, an exported binding, a stored field, or an argument to a serialize/`postMessage`/cache call. `@onrails/eslint-plugin` and `@onrails/biome-plugin` enforce this via `fluent-stays-local` (RFC 0002 §9).
|
|
30
31
|
|
|
31
32
|
## Sync / async interop
|
|
32
33
|
|
|
@@ -39,7 +40,7 @@ Two distinct lift paths:
|
|
|
39
40
|
|
|
40
41
|
```ts
|
|
41
42
|
asyncAfter(
|
|
42
|
-
trySync(() => Schema.parse(input), toError)
|
|
43
|
+
trySync(() => Schema.parse(input), toError),
|
|
43
44
|
(value) => tryAsync(save(value)),
|
|
44
45
|
);
|
|
45
46
|
```
|
package/README.md
CHANGED
|
@@ -11,9 +11,13 @@ bun add @onrails/result
|
|
|
11
11
|
## Quick start (value-first — best inference)
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import { err, flatMap, match, ok, trySync } from "@onrails/result";
|
|
14
|
+
import { err, flatMap, fromThrowable, match, ok, trySync } from "@onrails/result";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Run it now — the sync mirror of `tryAsync`.
|
|
17
|
+
const parsed = trySync(() => JSON.parse(raw)); // Result<unknown, Error>
|
|
18
|
+
|
|
19
|
+
// Or lift the throwing function once and reuse it.
|
|
20
|
+
const parse = fromThrowable(
|
|
17
21
|
(raw: string) => JSON.parse(raw),
|
|
18
22
|
(e) => ({ kind: "parse" as const, message: String(e) }),
|
|
19
23
|
);
|
|
@@ -21,24 +25,56 @@ const parse = trySync(
|
|
|
21
25
|
const pipeline = flatMap(parse('{"v":1}'), (data) => ok(data.v));
|
|
22
26
|
```
|
|
23
27
|
|
|
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`.
|
|
25
|
-
|
|
26
28
|
For worked examples of multi-step pipelines, parser builders, validator ladders, and parallel sub-workflows see [RECIPES.md](./RECIPES.md).
|
|
27
29
|
|
|
28
|
-
##
|
|
30
|
+
## The default path
|
|
31
|
+
|
|
32
|
+
Two forms cover almost everything. Learn these; treat the rest of this README as reference for the cases they do not fit.
|
|
33
|
+
|
|
34
|
+
| Sync | data-first module functions — `flatMap(r, fn)`, `map`, `match` |
|
|
35
|
+
| ----- | -------------------------------------------------------------- |
|
|
36
|
+
| Async | `ResultAsync` dot-chaining — `ra.flatMap(fn).map(fn).match(…)` |
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// sync
|
|
40
|
+
const name = match(
|
|
41
|
+
flatMap(parseConfig(raw), (cfg) => (cfg.name ? ok(cfg.name) : err({ kind: "missing" }))),
|
|
42
|
+
(n) => n,
|
|
43
|
+
(e) => `<${e.kind}>`,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// async — `.flatMap` also accepts a plain sync `Result`, so no manual lifting
|
|
47
|
+
const count = await loadUser(id).flatMap(loadOrders).map((os) => os.length).unwrapOr(0);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Data-first gives the best inference, and `ResultAsync.flatMap` takes either carrier, so mixed sync/async chains need no lift step.
|
|
51
|
+
|
|
52
|
+
## Other syntaxes (opt-in)
|
|
53
|
+
|
|
54
|
+
Same operations, different shape. Reach for one only when it removes real nesting or naming pain — none of them unlock behavior the default path lacks.
|
|
29
55
|
|
|
30
56
|
| Shape | Reach for |
|
|
31
57
|
| ---------------------------------- | -------------------------------------------------------------------- |
|
|
32
|
-
| One or two sync steps | `flatMapResult`, `mapResult`, `match` |
|
|
33
|
-
| One or two async steps | `ResultAsync.flatMap`, `asyncAfter` |
|
|
34
58
|
| Long sync chain, value-first | `pipe(r, map(...), flatMap(...), ...)` |
|
|
59
|
+
| Long async chain, value-first | `pipe(ra, RA.map(...), RA.flatMap(...))` from `@onrails/result/async` |
|
|
35
60
|
| Long sync chain, dot-style preferred | `fluent(r)` from `@onrails/result/fluent` |
|
|
36
61
|
| Reusable composed function | `flow(...)` from `@onrails/result/pipe` |
|
|
37
62
|
| Several named sync/async steps | `Railway.*` builder from `@onrails/result/railway` |
|
|
38
63
|
| Linear sync with early-return feel | `tryGen` + `$` from `@onrails/result/try-gen` |
|
|
64
|
+
|
|
65
|
+
Every transform is dual-form: data-first `flatMap(r, fn)` or curried `flatMap(fn)(r)` for `pipe` / `flow`.
|
|
66
|
+
|
|
67
|
+
## Boundaries and collections
|
|
68
|
+
|
|
69
|
+
These are not alternative syntaxes — they are the entry points into the railway.
|
|
70
|
+
|
|
71
|
+
| Shape | Reach for |
|
|
72
|
+
| ---------------------------------- | -------------------------------------------------------------------- |
|
|
39
73
|
| Independent validations, accumulated failures | `validateAll` / `validateTuple` from `@onrails/result` |
|
|
40
74
|
| Sync → async lift, keep error type | `fromResult`, `asyncAfter` (do **not** use `fromAsync` here) |
|
|
41
75
|
| `Promise<Result<…>>` boundary lift | `fromAsync` / `tryAsync` |
|
|
76
|
+
| Throwing sync call, run it now | `trySync(() => f())` — mirrors `tryAsync(promise)` |
|
|
77
|
+
| Throwing sync function, reusable | `fromThrowable(f, onThrow)` — neverthrow's `Result.fromThrowable` |
|
|
42
78
|
|
|
43
79
|
Rule of thumb: pick the smallest tool that removes nesting. Reach for `Railway` only when named context replaces positional tuple plumbing.
|
|
44
80
|
|
|
@@ -60,7 +96,7 @@ Use `asyncAfter` for the common "validate synchronously, then run async IO" shap
|
|
|
60
96
|
import { asyncAfter, tryAsync, trySync } from "@onrails/result";
|
|
61
97
|
|
|
62
98
|
return asyncAfter(
|
|
63
|
-
trySync(() => ArtifactSchema.parse(artifact), toError)
|
|
99
|
+
trySync(() => ArtifactSchema.parse(artifact), toError),
|
|
64
100
|
(validated) =>
|
|
65
101
|
tryAsync(
|
|
66
102
|
getDb()
|
|
@@ -94,6 +130,36 @@ type BotError =
|
|
|
94
130
|
|
|
95
131
|
Helpers: `@onrails/result/extra` — `hasKind`, `mapErrKind`, `declareErrors`, `UnionErrors`, `AccumulateErrors`.
|
|
96
132
|
|
|
133
|
+
### Literals stay narrow
|
|
134
|
+
|
|
135
|
+
`ok` and `err` lock an inline literal instead of widening it, so a tagged error keeps its `kind` without `as const`:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
err({ kind: "not_found", id }); // Result<never, { readonly kind: "not_found"; readonly id: string }>
|
|
139
|
+
ok("ready"); // Result<"ready", never>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Locking is inference-only — a value that already has a type is left alone (`ok(name)` where `name: string` is still `Result<string, never>`), and an array literal payload stays assignable to a mutable array (`ok([1, 2])` satisfies `Result<number[], E>`).
|
|
143
|
+
|
|
144
|
+
Pass **both** type arguments to opt out. Generic code that has to produce `Result<T, E>` for an unresolved `T` needs that form:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const lift = <T, E>(value: T): Result<T, E> => ok<T, E>(value);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Naming the error channel
|
|
151
|
+
|
|
152
|
+
One explicit type argument on `err` names the **error**:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
type AppError = { kind: "parse" } | { kind: "io" };
|
|
156
|
+
|
|
157
|
+
err<AppError>({ kind: "io" }); // Result<never, AppError>
|
|
158
|
+
err<number, AppError>({ kind: "io" }); // Result<number, AppError> — neverthrow's T, E order
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The two-argument form keeps neverthrow's order. `errAsync` / `ResultAsync.err` behave the same way.
|
|
162
|
+
|
|
97
163
|
## Async interop — `fromAsync`
|
|
98
164
|
|
|
99
165
|
Lift `async` handlers that return `Result` without leaking `Promise<Result<…>>`:
|
|
@@ -132,6 +198,20 @@ import { match } from "ts-pattern";
|
|
|
132
198
|
R.match(result, onOk, onErr);
|
|
133
199
|
```
|
|
134
200
|
|
|
201
|
+
For an object-form, self-labelling fold, use `matchTag` from [`@onrails/pattern`](../pattern/README.md) — it dispatches on `_tag` and requires one branch per tag, so the branches read as names instead of argument positions:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { matchTag } from "@onrails/pattern";
|
|
205
|
+
|
|
206
|
+
const label = (r: Result<number, string>) =>
|
|
207
|
+
matchTag(r, {
|
|
208
|
+
Ok: (v) => `ok:${v.value}`, // v: Ok<number> — the member, not the payload
|
|
209
|
+
Err: (e) => `err:${e.error}`, // e: Err<string>
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Note the difference: `match` hands each branch the *payload*, `matchTag` hands it the narrowed *member*.
|
|
214
|
+
|
|
135
215
|
`unwrapOk` and `unwrapErr` are test/assertion helpers. Prefer `match`, `isOk`, or `isErr` in production control flow.
|
|
136
216
|
|
|
137
217
|
```ts
|
|
@@ -163,7 +243,7 @@ import { ResultAsync } from "@onrails/result";
|
|
|
163
243
|
const combined = ResultAsync.combineTuple([
|
|
164
244
|
loadSettings(),
|
|
165
245
|
loadModelCatalog(),
|
|
166
|
-
]
|
|
246
|
+
]);
|
|
167
247
|
|
|
168
248
|
const dto = combined.map(([settings, catalog]) =>
|
|
169
249
|
buildDto(settings, catalog),
|
|
@@ -172,6 +252,25 @@ const dto = combined.map(([settings, catalog]) =>
|
|
|
172
252
|
|
|
173
253
|
When TS only infers the first error in a generator-style flow, use `declareErrors<E1 | E2>()` from `/extra`.
|
|
174
254
|
|
|
255
|
+
## Async pipelines — `@onrails/result/async`
|
|
256
|
+
|
|
257
|
+
`ResultAsync` is a class, so dot-chaining is always available and stays the shortest form for a one-off chain. When you want the sync side's *other* two syntaxes on the async carrier — value-first `pipe`, or point-free `flow` — import the data-last twins as a namespace:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import * as RA from "@onrails/result/async";
|
|
261
|
+
import { pipe } from "@onrails/result";
|
|
262
|
+
import { flow } from "@onrails/result/pipe";
|
|
263
|
+
|
|
264
|
+
// value-first
|
|
265
|
+
pipe(loadUser(id), RA.flatMap(loadOrders), RA.map((os) => os.length));
|
|
266
|
+
// ResultAsync<number, NotFound | DbError>
|
|
267
|
+
|
|
268
|
+
// reusable point-free pipeline — no value yet
|
|
269
|
+
const orderCount = flow(loadUser, RA.flatMap(loadOrders), RA.map((os) => os.length));
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Every `ResultAsync` method has a twin here — `map`, `mapErr`, `bimap`, `flatMap`, `recover`, `tap`, `tapErr`, `match`, `unwrapOr` — each in both data-first and curried form. The terminals (`match`, `unwrapOr`) resolve to a plain `Promise`, ending the pipeline.
|
|
273
|
+
|
|
175
274
|
## `Railway` — named service workflows
|
|
176
275
|
|
|
177
276
|
Use `Railway` from `@onrails/result/railway` when a service workflow has several named sync/async steps and would otherwise need manual context-carrying objects:
|
|
@@ -195,7 +294,15 @@ const summary = Railway.fromSync("profileId", () => ProfileIdSchema.parse(id), t
|
|
|
195
294
|
|
|
196
295
|
Sync-only workflows return `Result<T, E>`. The first `fromPromise`, `fromAsync`, or `parallel` step upgrades the output to `ResultAsync<T, E>`.
|
|
197
296
|
|
|
198
|
-
|
|
297
|
+
Step keys must be unique. Reusing one is a compile error naming the offending key — previously it overwrote at runtime while the type silently collapsed to `never`:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
Railway.empty()
|
|
301
|
+
.derive("x", () => 1)
|
|
302
|
+
.derive("x", () => "two"); // ✗ key "x" already exists in the workflow context
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Use lower-level helpers (`asyncAfter`, `fromResult`, `flatMap`) for one or two steps where a builder would add ceremony.
|
|
199
306
|
|
|
200
307
|
To share steps across workflows, extract plain functions of the context and plug them in via `.fromResult` / `.fromAsync`:
|
|
201
308
|
|
|
@@ -211,6 +318,8 @@ const summary = Railway.fromSync("profileId", () => ProfileIdSchema.parse(id), t
|
|
|
211
318
|
|
|
212
319
|
## Pipe
|
|
213
320
|
|
|
321
|
+
`pipe` and `flow` both live in `@onrails/result/pipe`; `pipe` is also re-exported from the package root.
|
|
322
|
+
|
|
214
323
|
```ts
|
|
215
324
|
import { pipe } from "@onrails/result";
|
|
216
325
|
import { flow } from "@onrails/result/pipe";
|
|
@@ -233,10 +342,53 @@ const parseUserName = flow(
|
|
|
233
342
|
parseUserName(raw);
|
|
234
343
|
```
|
|
235
344
|
|
|
345
|
+
Both cap at **12 steps**. That cap is deliberate: a single recursive variadic signature would remove it, but it cannot give each step a contextual type from the previous step's output, so lambda parameters degrade to `unknown` (`map((cfg) => cfg.user)` stops inferring). Longer chains nest — `pipe(pipe(v, ...), ...)` — or factor a segment into a named `flow`.
|
|
346
|
+
|
|
236
347
|
## ESLint
|
|
237
348
|
|
|
238
349
|
`@onrails/eslint-plugin` — warns on `Promise<Result<…>>` and `_unsafeUnwrap*`.
|
|
239
350
|
|
|
351
|
+
## Breaking changes
|
|
352
|
+
|
|
353
|
+
### `ok` / `err` lock inline literals
|
|
354
|
+
|
|
355
|
+
Inline literals no longer widen: `ok("a")` is `Result<"a", never>` and `err({ kind: "io" })` carries `{ readonly kind: "io" }`. Annotated values are unaffected. Two call shapes need a change:
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
// generic code — pass both type arguments to opt out of locking
|
|
359
|
+
const lift = <T, E>(value: T): Result<T, E> => ok<T, E>(value);
|
|
360
|
+
|
|
361
|
+
// a type-level assertion that expected the widened type
|
|
362
|
+
expectType<TypeEqual<typeof ok(1), Result<number, never>>>(true); // before
|
|
363
|
+
expectType<TypeEqual<typeof value, Result<1, never>>>(true); // after
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### One type argument on `err` now names the error
|
|
367
|
+
|
|
368
|
+
`err<AppError>(e)` used to set the *`Ok`* parameter and leave the error at `unknown`. It now returns `Result<never, AppError>`. Call sites that meant the old reading fail to compile; use `err<AppError, unknown>(e)` to restore it, or drop the argument and let inference lock the literal. The compat shim (`@onrails/result/compat/neverthrow`) is unchanged.
|
|
369
|
+
|
|
370
|
+
### `trySync` is now eager; the lazy form is `fromThrowable`
|
|
371
|
+
|
|
372
|
+
`trySync` used to lift a throwing *function* and return a wrapper, while
|
|
373
|
+
`tryAsync` took a *value* and ran immediately — same prefix, opposite contract.
|
|
374
|
+
The two now agree, and the lifting form takes neverthrow's own name.
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
// before
|
|
378
|
+
const parse = trySync(JSON.parse, toErr);
|
|
379
|
+
parse(raw);
|
|
380
|
+
trySync(() => Schema.parse(input), toErr)(); // note the trailing ()
|
|
381
|
+
|
|
382
|
+
// after
|
|
383
|
+
const parse = fromThrowable(JSON.parse, toErr);
|
|
384
|
+
parse(raw);
|
|
385
|
+
trySync(() => Schema.parse(input), toErr); // runs now, returns Result
|
|
386
|
+
trySync(() => Schema.parse(input)); // Result<T, Error> — onThrow optional
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Mechanical migration: rename `trySync` → `fromThrowable` everywhere, then drop
|
|
390
|
+
the trailing `()` on any call site that immediately invoked the wrapper.
|
|
391
|
+
|
|
240
392
|
## Migration from neverthrow
|
|
241
393
|
|
|
242
394
|
See [@onrails/codemod](../codemod/README.md) for the automated codemod, and the **Compat surface** notes below.
|
|
@@ -250,7 +402,7 @@ import { ResultAsync, Result, ok, err, okAsync, errAsync } from "@onrails/result
|
|
|
250
402
|
- `Result` / `ResultAsync` are class-shaped (`CompatResult` / `CompatResultAsync`).
|
|
251
403
|
- `await ra` resolves to a `CompatResult<T, E>` (thenable), so `.isOk()`, `.value`, `.error`, `.match()`, `.unwrapOr()` all work without an extra `.resolve()` call.
|
|
252
404
|
- `andThen` / `chain` / `flatMap` / `orElse` accept any of `CompatResultAsync` / `ResultAsync` / `CompatResult` / tagged `Result` returns and union the error type.
|
|
253
|
-
- Supported: `andThen`, `asyncAndThen`, `chain`, `flatMap`, `
|
|
405
|
+
- Supported: `andThen`, `asyncAndThen`, `chain`, `flatMap`, `map`, `mapErr`, `orElse`, `match`, `unwrapOr`, `isOk`, `isErr`, `andTee`, `orTee`, `Result.combine`, `Result.fromThrowable`, `ResultAsync.combine`, `ResultAsync.fromPromise`, `ResultAsync.fromSafePromise`, `ResultAsync.fromThrowable`, `_unsafeUnwrap` / `_unsafeUnwrapErr`.
|
|
254
406
|
- Treat the compat surface as a migration step, not the destination — once a package migrates, switch its imports to `@onrails/result` and `@onrails/result/fluent`.
|
|
255
407
|
|
|
256
408
|
## Subpaths
|
|
@@ -258,9 +410,10 @@ import { ResultAsync, Result, ok, err, okAsync, errAsync } from "@onrails/result
|
|
|
258
410
|
| Path | Contents |
|
|
259
411
|
|------|----------|
|
|
260
412
|
| `@onrails/result` | Core + interop exports |
|
|
261
|
-
| `@onrails/result/
|
|
413
|
+
| `@onrails/result/async` | Data-last twins of the `ResultAsync` methods (for `pipe` / `flow`) |
|
|
414
|
+
| `@onrails/result/fluent` | `fluent()` |
|
|
262
415
|
| `@onrails/result/extra` | Error-type utilities |
|
|
263
|
-
| `@onrails/result/pipe` | `flow` (
|
|
416
|
+
| `@onrails/result/pipe` | `pipe` (value-first) and `flow` (point-free), up to 12 steps |
|
|
264
417
|
| `@onrails/result/railway` | `Railway` named-context workflow builder |
|
|
265
418
|
| `@onrails/result/try-gen` | `tryGen`, `yieldResult`, `$` |
|
|
266
419
|
| `@onrails/result/compat/neverthrow` | Migration shim |
|
package/RECIPES.md
CHANGED
|
@@ -81,15 +81,17 @@ Define a parser factory once, instantiate per schema.
|
|
|
81
81
|
|
|
82
82
|
```ts
|
|
83
83
|
import { flow } from "@onrails/result/pipe";
|
|
84
|
-
import { flatMap,
|
|
84
|
+
import { flatMap, fromThrowable, type Result } from "@onrails/result";
|
|
85
85
|
|
|
86
86
|
type ParseError = { kind: "parse"; message: string };
|
|
87
87
|
type SchemaError = { kind: "schema"; field: string };
|
|
88
88
|
|
|
89
89
|
const parseJsonWith = <T>(schema: { parse: (x: unknown) => T }) =>
|
|
90
90
|
flow(
|
|
91
|
-
|
|
92
|
-
flatMap(
|
|
91
|
+
fromThrowable(JSON.parse, (e): ParseError => ({ kind: "parse", message: String(e) })),
|
|
92
|
+
flatMap(
|
|
93
|
+
fromThrowable(schema.parse, (e): SchemaError => ({ kind: "schema", field: String(e) })),
|
|
94
|
+
),
|
|
93
95
|
);
|
|
94
96
|
|
|
95
97
|
const parseUser = parseJsonWith(UserSchema);
|
|
@@ -208,7 +210,7 @@ const buildSummary = (userId: string) =>
|
|
|
208
210
|
loadProfile(userId), // ResultAsync<Profile, ProfileError>
|
|
209
211
|
loadRecentMetrics(userId), // ResultAsync<Metrics, MetricsError>
|
|
210
212
|
loadFeatureFlags(userId), // ResultAsync<Flags, never>
|
|
211
|
-
]
|
|
213
|
+
]),
|
|
212
214
|
(combined) =>
|
|
213
215
|
combined.map(([profile, metrics, flags]) => ({
|
|
214
216
|
userId,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/internal/dual.ts
|
|
4
|
+
var dual = (arity, body) => {
|
|
5
|
+
const run = body;
|
|
6
|
+
const dispatcher = (...args) => args.length >= arity ? run(...args) : (self) => run(self, ...args);
|
|
7
|
+
return dispatcher;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// src/async-fns.ts
|
|
11
|
+
var map = dual(2, (ra, fn) => ra.map(fn));
|
|
12
|
+
var mapErr = dual(
|
|
13
|
+
2,
|
|
14
|
+
(ra, fn) => ra.mapErr(fn)
|
|
15
|
+
);
|
|
16
|
+
var bimap = dual(
|
|
17
|
+
3,
|
|
18
|
+
(ra, onOk, onErr) => ra.bimap(onOk, onErr)
|
|
19
|
+
);
|
|
20
|
+
var flatMap = dual(
|
|
21
|
+
2,
|
|
22
|
+
(ra, fn) => ra.flatMap(fn)
|
|
23
|
+
);
|
|
24
|
+
var recover = dual(
|
|
25
|
+
2,
|
|
26
|
+
(ra, fn) => ra.recover(fn)
|
|
27
|
+
);
|
|
28
|
+
var tap = dual(2, (ra, fn) => ra.tap(fn));
|
|
29
|
+
var tapErr = dual(
|
|
30
|
+
2,
|
|
31
|
+
(ra, fn) => ra.tapErr(fn)
|
|
32
|
+
);
|
|
33
|
+
var match = dual(
|
|
34
|
+
3,
|
|
35
|
+
(ra, onOk, onErr) => ra.match(onOk, onErr)
|
|
36
|
+
);
|
|
37
|
+
var unwrapOr = dual(
|
|
38
|
+
2,
|
|
39
|
+
(ra, defaultValue) => ra.unwrapOr(defaultValue)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
exports.bimap = bimap;
|
|
43
|
+
exports.flatMap = flatMap;
|
|
44
|
+
exports.map = map;
|
|
45
|
+
exports.mapErr = mapErr;
|
|
46
|
+
exports.match = match;
|
|
47
|
+
exports.recover = recover;
|
|
48
|
+
exports.tap = tap;
|
|
49
|
+
exports.tapErr = tapErr;
|
|
50
|
+
exports.unwrapOr = unwrapOr;
|
|
51
|
+
//# sourceMappingURL=async-fns.cjs.map
|
|
52
|
+
//# sourceMappingURL=async-fns.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/dual.ts","../src/async-fns.ts"],"names":[],"mappings":";;;AA0CO,IAAM,IAAA,GAAO,CAAI,KAAA,EAAe,IAAA,KAA2C;AAMhF,EAAA,MAAM,GAAA,GAAM,IAAA;AACZ,EAAA,MAAM,UAAA,GAAa,CAAA,GAAI,IAAA,KACrB,IAAA,CAAK,UAAU,KAAA,GAAQ,GAAA,CAAI,GAAG,IAAI,IAAI,CAAC,IAAA,KAAkB,GAAA,CAAI,IAAA,EAAM,GAAG,IAAI,CAAA;AAG5E,EAAA,OAAO,UAAA;AACT,CAAA;;;ACbO,IAAM,GAAA,GAGT,KAAK,CAAA,EAAG,CAAU,IAAuB,EAAA,KAA2C,EAAA,CAAG,GAAA,CAAI,EAAE,CAAC;AAW3F,IAAM,MAAA,GAGT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CAAU,EAAA,EAAuB,EAAA,KAA2C,EAAA,CAAG,OAAO,EAAE;AAC1F;AAUO,IAAM,KAAA,GAUT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CACE,EAAA,EACA,IAAA,EACA,UACsB,EAAA,CAAG,KAAA,CAAM,MAAM,KAAK;AAC9C;AAWO,IAAM,OAAA,GAGT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CAAa,EAAA,EAAuB,EAAA,KAClC,EAAA,CAAG,QAAQ,EAAE;AACjB;AAWO,IAAM,OAAA,GAGT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CAAU,EAAA,EAAuB,EAAA,KAC/B,EAAA,CAAG,QAAQ,EAAE;AACjB;AAUO,IAAM,GAAA,GAGT,KAAK,CAAA,EAAG,CAAO,IAAuB,EAAA,KAA8C,EAAA,CAAG,GAAA,CAAI,EAAE,CAAC;AAU3F,IAAM,MAAA,GAGT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CAAO,EAAA,EAAuB,EAAA,KAA8C,EAAA,CAAG,OAAO,EAAE;AAC1F;AAWO,IAAM,KAAA,GAUT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CACE,EAAA,EACA,IAAA,EACA,UACqB,EAAA,CAAG,KAAA,CAAM,MAAM,KAAK;AAC7C;AAWO,IAAM,QAAA,GAGT,IAAA;AAAA,EACF,CAAA;AAAA,EACA,CAAU,EAAA,EAAuB,YAAA,KAAoC,EAAA,CAAG,SAAS,YAAY;AAC/F","file":"async-fns.cjs","sourcesContent":["/**\n * @onrails/result internal — owning module for the **dual-form idiom**.\n *\n * Every dual-form transform in this package (`map`, `flatMap`, `match`, …)\n * accepts either shape:\n *\n * data-first: `map(result, fn)` — full arity, runs immediately\n * curried: `map(fn)(result)` — arity minus the leading `self`, returns\n * a unary function awaiting the data\n *\n * {@link dual} is the single arity dispatcher those transforms derive from;\n * hand-rolled `args.length` checks are not allowed elsewhere.\n *\n * NOTE: a twin copy of this module lives at `packages/maybe/src/internal/dual.ts`\n * — keep the two in sync when editing.\n *\n * Internal only: not listed in package.json `exports` or tsup entries.\n */\n\n/**\n * Derive a dual-form (data-first / curried) function from a single\n * data-first `body`.\n *\n * When called with `args.length >= arity` the wrapper runs `body(...args)`\n * (data-first). With fewer arguments it returns `(self) => body(self, ...args)`\n * — the curried, data-last-`self` form used with `pipe`/`flow`.\n *\n * The public type is supplied entirely by the caller: annotate the exported\n * const with the exact overload set and let `F` be inferred from it.\n *\n * @example\n * ```ts\n * export const map: {\n * <T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E>;\n * <T, U>(fn: (value: T) => U): <E>(result: Result<T, E>) => Result<U, E>;\n * } = dual(\n * 2,\n * <T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> =>\n * isOk(result) ? ok(fn(result.value)) : err(result.error),\n * );\n * ```\n */\nexport const dual = <F>(arity: number, body: (...args: never[]) => unknown): F => {\n // `(...args: never[]) => unknown` is the top function type, so every typed\n // body is assignable to the parameter — but it cannot be invoked with\n // `unknown` arguments. Re-view it through the variadic shape the dispatcher\n // actually forwards; the constraint is that `body` only ever receives the\n // arguments the caller's overload annotation admitted.\n const run = body as (...args: readonly unknown[]) => unknown;\n const dispatcher = (...args: readonly unknown[]): unknown =>\n args.length >= arity ? run(...args) : (self: unknown) => run(self, ...args);\n // The caller's overload annotation on the exported const supplies the real\n // public type; internally the dispatcher stays `unknown`-typed.\n return dispatcher as F;\n};\n","/**\n * `@onrails/result/async` — data-last twins of the {@link ResultAsync} methods,\n * so an async railway composes with `pipe` / `flow` the same way a sync one does.\n *\n * `ResultAsync` is a class, so dot-chaining is always available and remains the\n * shortest form for a one-off chain. Reach for this module when you want the\n * sync side's other two syntaxes on the async carrier:\n *\n * ```ts\n * import * as RA from \"@onrails/result/async\";\n * import { pipe } from \"@onrails/result\";\n * import { flow } from \"@onrails/result/pipe\";\n *\n * // value-first pipe\n * pipe(loadUser(id), RA.flatMap(loadOrders), RA.map((os) => os.length));\n *\n * // reusable point-free pipeline — no value yet\n * const orderCount = flow(loadUser, RA.flatMap(loadOrders), RA.map((os) => os.length));\n * ```\n *\n * Every export mirrors one `ResultAsync` method exactly, in both forms:\n * data-first (`RA.map(ra, fn)`) and curried (`RA.map(fn)`). Import as a\n * namespace — the names deliberately match the sync core, and `test/parity.spec.ts`\n * enforces that the mirror stays complete.\n */\n\nimport type { ResultAsync } from \"./async.js\";\nimport { dual } from \"./internal/dual.js\";\nimport type { Result } from \"./types.js\";\n\n/** A step may hand back either carrier; the async one absorbs a sync `Result`. */\ntype Step<U, F> = ResultAsync<U, F> | Result<U, F>;\n\n/**\n * Transforms the `Ok` value, passing `Err` through unchanged.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), map((u) => u.name)); // ResultAsync<string, LoadError>\n * ```\n */\nexport const map: {\n <T, U, E>(ra: ResultAsync<T, E>, fn: (value: T) => U): ResultAsync<U, E>;\n <T, U>(fn: (value: T) => U): <E>(ra: ResultAsync<T, E>) => ResultAsync<U, E>;\n} = dual(2, <T, U, E>(ra: ResultAsync<T, E>, fn: (value: T) => U): ResultAsync<U, E> => ra.map(fn));\n\n/**\n * Transforms the `Err` value, passing `Ok` through unchanged — useful for\n * unifying heterogeneous failures into one app-level union.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), mapErr((e): AppError => ({ kind: \"load\", cause: e })));\n * ```\n */\nexport const mapErr: {\n <T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => F): ResultAsync<T, F>;\n <E, F>(fn: (error: E) => F): <T>(ra: ResultAsync<T, E>) => ResultAsync<T, F>;\n} = dual(\n 2,\n <T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => F): ResultAsync<T, F> => ra.mapErr(fn),\n);\n\n/**\n * Transforms both tracks in one step. Exactly one of `onOk` / `onErr` runs.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), bimap(toDto, (e): AppError => ({ kind: \"load\", cause: e })));\n * ```\n */\nexport const bimap: {\n <T, U, E, F>(\n ra: ResultAsync<T, E>,\n onOk: (value: T) => U,\n onErr: (error: E) => F,\n ): ResultAsync<U, F>;\n <T, U, E, F>(\n onOk: (value: T) => U,\n onErr: (error: E) => F,\n ): (ra: ResultAsync<T, E>) => ResultAsync<U, F>;\n} = dual(\n 3,\n <T, U, E, F>(\n ra: ResultAsync<T, E>,\n onOk: (value: T) => U,\n onErr: (error: E) => F,\n ): ResultAsync<U, F> => ra.bimap(onOk, onErr),\n);\n\n/**\n * Canonical bind — chains a step returning a `ResultAsync` or a sync `Result`,\n * short-circuiting on `Err`. Error types accumulate (`E | F`).\n *\n * @example\n * ```ts\n * pipe(authenticate(req), flatMap((user) => loadProfile(user.id)));\n * ```\n */\nexport const flatMap: {\n <T, U, E, F>(ra: ResultAsync<T, E>, fn: (value: T) => Step<U, F>): ResultAsync<U, E | F>;\n <T, U, F>(fn: (value: T) => Step<U, F>): <E>(ra: ResultAsync<T, E>) => ResultAsync<U, E | F>;\n} = dual(\n 2,\n <T, U, E, F>(ra: ResultAsync<T, E>, fn: (value: T) => Step<U, F>): ResultAsync<U, E | F> =>\n ra.flatMap(fn),\n);\n\n/**\n * Error-track bind — runs `fn` only on `Err`, letting a failed workflow\n * recover to `Ok` or remap the failure.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), recover((e) => (e === \"notFound\" ? okAsync(guest) : errAsync(e))));\n * ```\n */\nexport const recover: {\n <T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => Step<T, F>): ResultAsync<T, F>;\n <T, E, F>(fn: (error: E) => Step<T, F>): (ra: ResultAsync<T, E>) => ResultAsync<T, F>;\n} = dual(\n 2,\n <T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => Step<T, F>): ResultAsync<T, F> =>\n ra.recover(fn),\n);\n\n/**\n * Observes the `Ok` value for side effects without changing it.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), tap((u) => log.info({ msg: \"loaded\", id: u.id })));\n * ```\n */\nexport const tap: {\n <T, E>(ra: ResultAsync<T, E>, fn: (value: T) => void): ResultAsync<T, E>;\n <T>(fn: (value: T) => void): <E>(ra: ResultAsync<T, E>) => ResultAsync<T, E>;\n} = dual(2, <T, E>(ra: ResultAsync<T, E>, fn: (value: T) => void): ResultAsync<T, E> => ra.tap(fn));\n\n/**\n * Observes the `Err` value for side effects without changing it.\n *\n * @example\n * ```ts\n * pipe(loadUser(id), tapErr((e) => metrics.inc(\"user.load.fail\", { kind: e.kind })));\n * ```\n */\nexport const tapErr: {\n <T, E>(ra: ResultAsync<T, E>, fn: (error: E) => void): ResultAsync<T, E>;\n <E>(fn: (error: E) => void): <T>(ra: ResultAsync<T, E>) => ResultAsync<T, E>;\n} = dual(\n 2,\n <T, E>(ra: ResultAsync<T, E>, fn: (error: E) => void): ResultAsync<T, E> => ra.tapErr(fn),\n);\n\n/**\n * Terminal collapse — folds both tracks into a single awaited value, settling\n * the carrier exactly once. Ends the pipeline, so it returns a plain `Promise`.\n *\n * @example\n * ```ts\n * const status = await pipe(save(row), match(() => 200, (e) => (e.kind === \"conflict\" ? 409 : 500)));\n * ```\n */\nexport const match: {\n <T, E, U1, U2 = U1>(\n ra: ResultAsync<T, E>,\n onOk: (value: T) => U1,\n onErr: (error: E) => U2,\n ): Promise<U1 | U2>;\n <T, E, U1, U2 = U1>(\n onOk: (value: T) => U1,\n onErr: (error: E) => U2,\n ): (ra: ResultAsync<T, E>) => Promise<U1 | U2>;\n} = dual(\n 3,\n <T, E, U1, U2>(\n ra: ResultAsync<T, E>,\n onOk: (value: T) => U1,\n onErr: (error: E) => U2,\n ): Promise<U1 | U2> => ra.match(onOk, onErr),\n);\n\n/**\n * Terminal — resolves to the `Ok` value, or `defaultValue` on `Err`. The\n * fallback may differ from `T`; the result widens to `T | U`.\n *\n * @example\n * ```ts\n * const profile = await pipe(loadProfile(id), unwrapOr(guestProfile));\n * ```\n */\nexport const unwrapOr: {\n <T, E, U>(ra: ResultAsync<T, E>, defaultValue: U): Promise<T | U>;\n <U>(defaultValue: U): <T, E>(ra: ResultAsync<T, E>) => Promise<T | U>;\n} = dual(\n 2,\n <T, E, U>(ra: ResultAsync<T, E>, defaultValue: U): Promise<T | U> => ra.unwrapOr(defaultValue),\n);\n"]}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@onrails/result/async` — data-last twins of the {@link ResultAsync} methods,
|
|
3
|
+
* so an async railway composes with `pipe` / `flow` the same way a sync one does.
|
|
4
|
+
*
|
|
5
|
+
* `ResultAsync` is a class, so dot-chaining is always available and remains the
|
|
6
|
+
* shortest form for a one-off chain. Reach for this module when you want the
|
|
7
|
+
* sync side's other two syntaxes on the async carrier:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import * as RA from "@onrails/result/async";
|
|
11
|
+
* import { pipe } from "@onrails/result";
|
|
12
|
+
* import { flow } from "@onrails/result/pipe";
|
|
13
|
+
*
|
|
14
|
+
* // value-first pipe
|
|
15
|
+
* pipe(loadUser(id), RA.flatMap(loadOrders), RA.map((os) => os.length));
|
|
16
|
+
*
|
|
17
|
+
* // reusable point-free pipeline — no value yet
|
|
18
|
+
* const orderCount = flow(loadUser, RA.flatMap(loadOrders), RA.map((os) => os.length));
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Every export mirrors one `ResultAsync` method exactly, in both forms:
|
|
22
|
+
* data-first (`RA.map(ra, fn)`) and curried (`RA.map(fn)`). Import as a
|
|
23
|
+
* namespace — the names deliberately match the sync core, and `test/parity.spec.ts`
|
|
24
|
+
* enforces that the mirror stays complete.
|
|
25
|
+
*/
|
|
26
|
+
import type { ResultAsync } from "./async.js";
|
|
27
|
+
import type { Result } from "./types.js";
|
|
28
|
+
/** A step may hand back either carrier; the async one absorbs a sync `Result`. */
|
|
29
|
+
type Step<U, F> = ResultAsync<U, F> | Result<U, F>;
|
|
30
|
+
/**
|
|
31
|
+
* Transforms the `Ok` value, passing `Err` through unchanged.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* pipe(loadUser(id), map((u) => u.name)); // ResultAsync<string, LoadError>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const map: {
|
|
39
|
+
<T, U, E>(ra: ResultAsync<T, E>, fn: (value: T) => U): ResultAsync<U, E>;
|
|
40
|
+
<T, U>(fn: (value: T) => U): <E>(ra: ResultAsync<T, E>) => ResultAsync<U, E>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Transforms the `Err` value, passing `Ok` through unchanged — useful for
|
|
44
|
+
* unifying heterogeneous failures into one app-level union.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* pipe(loadUser(id), mapErr((e): AppError => ({ kind: "load", cause: e })));
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare const mapErr: {
|
|
52
|
+
<T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => F): ResultAsync<T, F>;
|
|
53
|
+
<E, F>(fn: (error: E) => F): <T>(ra: ResultAsync<T, E>) => ResultAsync<T, F>;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Transforms both tracks in one step. Exactly one of `onOk` / `onErr` runs.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* pipe(loadUser(id), bimap(toDto, (e): AppError => ({ kind: "load", cause: e })));
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare const bimap: {
|
|
64
|
+
<T, U, E, F>(ra: ResultAsync<T, E>, onOk: (value: T) => U, onErr: (error: E) => F): ResultAsync<U, F>;
|
|
65
|
+
<T, U, E, F>(onOk: (value: T) => U, onErr: (error: E) => F): (ra: ResultAsync<T, E>) => ResultAsync<U, F>;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Canonical bind — chains a step returning a `ResultAsync` or a sync `Result`,
|
|
69
|
+
* short-circuiting on `Err`. Error types accumulate (`E | F`).
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* pipe(authenticate(req), flatMap((user) => loadProfile(user.id)));
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare const flatMap: {
|
|
77
|
+
<T, U, E, F>(ra: ResultAsync<T, E>, fn: (value: T) => Step<U, F>): ResultAsync<U, E | F>;
|
|
78
|
+
<T, U, F>(fn: (value: T) => Step<U, F>): <E>(ra: ResultAsync<T, E>) => ResultAsync<U, E | F>;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Error-track bind — runs `fn` only on `Err`, letting a failed workflow
|
|
82
|
+
* recover to `Ok` or remap the failure.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* pipe(loadUser(id), recover((e) => (e === "notFound" ? okAsync(guest) : errAsync(e))));
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare const recover: {
|
|
90
|
+
<T, E, F>(ra: ResultAsync<T, E>, fn: (error: E) => Step<T, F>): ResultAsync<T, F>;
|
|
91
|
+
<T, E, F>(fn: (error: E) => Step<T, F>): (ra: ResultAsync<T, E>) => ResultAsync<T, F>;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Observes the `Ok` value for side effects without changing it.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* pipe(loadUser(id), tap((u) => log.info({ msg: "loaded", id: u.id })));
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare const tap: {
|
|
102
|
+
<T, E>(ra: ResultAsync<T, E>, fn: (value: T) => void): ResultAsync<T, E>;
|
|
103
|
+
<T>(fn: (value: T) => void): <E>(ra: ResultAsync<T, E>) => ResultAsync<T, E>;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Observes the `Err` value for side effects without changing it.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* pipe(loadUser(id), tapErr((e) => metrics.inc("user.load.fail", { kind: e.kind })));
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export declare const tapErr: {
|
|
114
|
+
<T, E>(ra: ResultAsync<T, E>, fn: (error: E) => void): ResultAsync<T, E>;
|
|
115
|
+
<E>(fn: (error: E) => void): <T>(ra: ResultAsync<T, E>) => ResultAsync<T, E>;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Terminal collapse — folds both tracks into a single awaited value, settling
|
|
119
|
+
* the carrier exactly once. Ends the pipeline, so it returns a plain `Promise`.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* const status = await pipe(save(row), match(() => 200, (e) => (e.kind === "conflict" ? 409 : 500)));
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export declare const match: {
|
|
127
|
+
<T, E, U1, U2 = U1>(ra: ResultAsync<T, E>, onOk: (value: T) => U1, onErr: (error: E) => U2): Promise<U1 | U2>;
|
|
128
|
+
<T, E, U1, U2 = U1>(onOk: (value: T) => U1, onErr: (error: E) => U2): (ra: ResultAsync<T, E>) => Promise<U1 | U2>;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Terminal — resolves to the `Ok` value, or `defaultValue` on `Err`. The
|
|
132
|
+
* fallback may differ from `T`; the result widens to `T | U`.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const profile = await pipe(loadProfile(id), unwrapOr(guestProfile));
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export declare const unwrapOr: {
|
|
140
|
+
<T, E, U>(ra: ResultAsync<T, E>, defaultValue: U): Promise<T | U>;
|
|
141
|
+
<U>(defaultValue: U): <T, E>(ra: ResultAsync<T, E>) => Promise<T | U>;
|
|
142
|
+
};
|
|
143
|
+
export {};
|