@onrails/result 0.2.0 → 0.3.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.
Files changed (60) hide show
  1. package/DESIGN.md +11 -18
  2. package/README.md +14 -59
  3. package/RECIPES.md +12 -25
  4. package/dist/{async-1fIhfzFu.d.ts → async-Ci6WSL5-.d.ts} +60 -18
  5. package/dist/{async-u8U5P2EH.d.cts → async-DcXR7k9K.d.cts} +60 -18
  6. package/dist/compat/neverthrow.cjs +42 -51
  7. package/dist/compat/neverthrow.cjs.map +1 -1
  8. package/dist/compat/neverthrow.d.cts +3 -3
  9. package/dist/compat/neverthrow.d.ts +3 -3
  10. package/dist/compat/neverthrow.js +42 -51
  11. package/dist/compat/neverthrow.js.map +1 -1
  12. package/dist/extra.cjs +1 -1
  13. package/dist/extra.cjs.map +1 -1
  14. package/dist/extra.d.cts +5 -11
  15. package/dist/extra.d.ts +5 -11
  16. package/dist/extra.js +1 -1
  17. package/dist/extra.js.map +1 -1
  18. package/dist/fluent.cjs +66 -28
  19. package/dist/fluent.cjs.map +1 -1
  20. package/dist/fluent.d.cts +41 -6
  21. package/dist/fluent.d.ts +41 -6
  22. package/dist/fluent.js +66 -28
  23. package/dist/fluent.js.map +1 -1
  24. package/dist/index.cjs +95 -95
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +168 -44
  27. package/dist/index.d.ts +168 -44
  28. package/dist/index.js +90 -95
  29. package/dist/index.js.map +1 -1
  30. package/dist/railway.cjs +101 -172
  31. package/dist/railway.cjs.map +1 -1
  32. package/dist/railway.d.cts +25 -179
  33. package/dist/railway.d.ts +25 -179
  34. package/dist/railway.js +102 -162
  35. package/dist/railway.js.map +1 -1
  36. package/dist/try-gen.cjs +1 -1
  37. package/dist/try-gen.cjs.map +1 -1
  38. package/dist/try-gen.js +1 -1
  39. package/dist/try-gen.js.map +1 -1
  40. package/package.json +1 -16
  41. package/skills/railway-do-notation/SKILL.md +29 -68
  42. package/skills/result-composition/SKILL.md +2 -4
  43. package/dist/$.cjs +0 -40
  44. package/dist/$.cjs.map +0 -1
  45. package/dist/$.js +0 -36
  46. package/dist/$.js.map +0 -1
  47. package/dist/_.d.cts +0 -2
  48. package/dist/_.d.ts +0 -2
  49. package/dist/interop.cjs +0 -433
  50. package/dist/interop.cjs.map +0 -1
  51. package/dist/interop.d.cts +0 -75
  52. package/dist/interop.d.ts +0 -75
  53. package/dist/interop.js +0 -429
  54. package/dist/interop.js.map +0 -1
  55. package/dist/validation.cjs +0 -53
  56. package/dist/validation.cjs.map +0 -1
  57. package/dist/validation.d.cts +0 -72
  58. package/dist/validation.d.ts +0 -72
  59. package/dist/validation.js +0 -48
  60. 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
@@ -22,10 +22,11 @@ type Result<T, E> =
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
+ - `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).
25
26
  - `tap` / `tapErr` observe a track without changing the carried value.
26
27
  - `recover` binds the error track and may return a failed workflow back to success.
27
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`.
28
- - Optional dot-chaining via `fluent(r)` / `fluentAsync(ra)` in `@onrails/result/fluent`.
29
+ - 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
30
 
30
31
  ## Sync / async interop
31
32
 
@@ -62,24 +63,18 @@ type RailwayOutput<T, E, M extends RailwayMode> =
62
63
  M extends "async" ? ResultAsync<T, E> : Result<T, E>;
63
64
  ```
64
65
 
65
- Use `Railway` when named context removes nesting or positional tuple plumbing. Prefer `asyncAfter`, `fromResult`, or direct `flatMap` for one- or two-step flows.
66
+ 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
67
 
67
- `railway(input, ...steps)` is the functional companion for reusable workflow steps. Step factories: `parseWith(parser, onThrow).as(key)`, `fromSyncNamed`, `fromResultNamed`, `fromPromiseNamed`, `fromAsyncNamed`, `deriveNamed`, `requireNamed`, `parallelNamed`, `select`.
68
+ 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
69
 
69
70
  ## Combining results
70
71
 
71
- - `combine` / `combineTuple` — sync, first-Err wins.
72
- - `ResultAsync.combineTuple` — 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
72
+ One module (`collections.ts`), one naming matrix — short-circuit vs accumulate × array vs tuple:
77
73
 
78
- `@onrails/result/validation` is a separate surface for accumulated independent failures (vs. railway short-circuit).
79
-
80
- - `validateAll(results, join)` — combine errors via a join function.
81
- - `validateAllArray(results)` — collect all errors into a readonly array.
82
- - `validateTupleArray(results)` — same for heterogeneous tuple shapes.
74
+ - `combine` / `combineTuple` — sync, first-Err wins.
75
+ - `validateAll` / `validateTuple` — accumulate independent failures; optional `combineErrors` fold, default collects a `readonly E[]`.
76
+ - `ResultAsync.combine` / `ResultAsync.combineTuple` — async, sequential, first-Err in input order.
77
+ - `ResultAsync.combineTupleParallel` — async, concurrent (branches overlap), first-Err in input order.
83
78
 
84
79
  Use `flatMap` for dependent checks where later checks need earlier successful values.
85
80
 
@@ -95,11 +90,9 @@ Use `flatMap` for dependent checks where later checks need earlier successful va
95
90
  | `@onrails/result` | Core: dual-form map/flatMap/match, variadic `pipe`, sync collection, async surface, lift helpers, generator sugar |
96
91
  | `@onrails/result/fluent` | Dot-style chains |
97
92
  | `@onrails/result/extra` | Error-type utilities |
98
- | `@onrails/result/interop` | Promise/ResultAsync boundary helpers |
99
93
  | `@onrails/result/pipe` | Variadic `flow` for point-free composition |
100
- | `@onrails/result/railway` | Named workflow builder and reusable step factories |
94
+ | `@onrails/result/railway` | Named workflow builder |
101
95
  | `@onrails/result/try-gen` | Sync generator-style `Result` sugar |
102
- | `@onrails/result/validation` | Independent validation with accumulated failures |
103
96
  | `@onrails/result/compat/neverthrow` | Migration shim — class-shaped surface for incremental migration off neverthrow |
104
97
 
105
98
  ## 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 = flatMapResult(parse('{"v":1}'), (data) => ok(data.v));
21
+ const pipeline = flatMap(parse('{"v":1}'), (data) => ok(data.v));
31
22
  ```
32
23
 
33
- Long chains: `fluent()` from `@onrails/result/fluent` or `flatMapResult` (not curried `flatMap`) for TS inference.
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.*` (fluent) or `railway(...)` (functional, reusable steps) |
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/validation` |
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
 
@@ -164,7 +155,7 @@ const out = tryGen(() => {
164
155
  });
165
156
  ```
166
157
 
167
- Use `ResultAsync.combineTuple` (or `parallelTupleAsync` when branches should overlap) when combining heterogeneous async results and destructuring the result:
158
+ Use `ResultAsync.combineTuple` (or `ResultAsync.combineTupleParallel` when branches should overlap) when combining heterogeneous async results and destructuring the result:
168
159
 
169
160
  ```ts
170
161
  import { ResultAsync } from "@onrails/result";
@@ -206,53 +197,18 @@ Sync-only workflows return `Result<T, E>`. The first `fromPromise`, `fromAsync`,
206
197
 
207
198
  Use lower-level helpers (`asyncAfter`, `fromResult`, `flatMapResult`) for one or two steps where a builder would add ceremony.
208
199
 
209
- ## `railway(...)` — reusable workflow steps
210
-
211
- 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`:
212
201
 
213
202
  ```ts
214
- import {
215
- deriveNamed,
216
- fromPromiseNamed,
217
- parallelNamed,
218
- parseWith,
219
- railway,
220
- requireNamed,
221
- select,
222
- } from "@onrails/result/railway";
223
-
224
- const parseProfileId = parseWith(ProfileIdSchema, toError).as("profileId");
225
-
226
- const loadProfileRow = fromPromiseNamed(
227
- "row",
228
- ({ profileId }) => loadProfileRowById(profileId),
229
- toError,
230
- );
231
-
232
- const requireProfile = requireNamed("profile", "row", ({ profileId }) =>
233
- new Error(`Profile not found: ${profileId}`),
234
- );
235
-
236
- const loadSummaryInputs = parallelNamed({
237
- recentArtifacts: ({ profile }) => loadRecentArtifacts(profile.id),
238
- jobMetrics: ({ profile }) => loadJobMetrics(profile.id),
239
- });
203
+ const loadProfileRow = ({ profileId }: { profileId: string }) =>
204
+ tryAsync(loadProfileRowById(profileId), toError);
240
205
 
241
- const summary = railway(
242
- id,
243
- parseProfileId,
244
- loadProfileRow,
245
- requireProfile,
246
- deriveNamed("normalized", ({ profile }) => normalizeProfile(profile)),
247
- loadSummaryInputs,
248
- select(({ normalized, recentArtifacts, jobMetrics }) =>
249
- toProfileSummary({ normalized, recentArtifacts, jobMetrics }),
250
- ),
251
- );
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));
252
210
  ```
253
211
 
254
- `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`.
255
-
256
212
  ## Pipe
257
213
 
258
214
  ```ts
@@ -304,9 +260,8 @@ import { ResultAsync, Result, ok, err, okAsync, errAsync } from "@onrails/result
304
260
  | `@onrails/result` | Core + interop exports |
305
261
  | `@onrails/result/fluent` | `fluent()`, `fluentAsync()` |
306
262
  | `@onrails/result/extra` | Error-type utilities |
307
- | `@onrails/result/interop` | `fromAsync`, `fromResult`, `asyncAfter` |
308
263
  | `@onrails/result/pipe` | `flow` (variadic point-free composition) |
309
- | `@onrails/result/railway` | `Railway`, `railway`, named workflow helpers |
264
+ | `@onrails/result/railway` | `Railway` named-context workflow builder |
310
265
  | `@onrails/result/try-gen` | `tryGen`, `yieldResult`, `$` |
311
266
  | `@onrails/result/compat/neverthrow` | Migration shim |
312
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` or `railway()` steps |
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, parallelTupleAsync } from "@onrails/result";
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
- parallelTupleAsync([
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
- `parallelTupleAsync` preserves tuple positions, so destructuring stays type-safe. Use static `ResultAsync.combineTuple` if branches must run left-to-right.
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. Functional Railway pipelines (`railway` + named steps)
438
+ ## 13. Railway workflow pipelines (named-context builder)
439
439
 
440
- Use `railway(input, ...steps)` from `@onrails/result/railway` to build a multi-step async pipeline using point-free reusable step wrappers. If any step is async, the entire pipeline resolves to a `ResultAsync`.
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 { railway, parseNamed, fromPromiseNamed, deriveNamed, select } from "@onrails/result/railway";
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
- railway(
458
- rawId,
459
- parseNamed("id", IdSchema, toError),
460
- fromPromiseNamed(
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 now reads name-first (`parseNamed("id", …)`, `fromPromiseNamed("profile", …)`, `deriveNamed("title", …)`) so the field names line up at the left edge. `parseWith(schema).as("id")` remains for the fluent trailing-name style.
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
 
@@ -1,12 +1,51 @@
1
1
  import { R as Result, U as UnexpectedError } from './types-BQ9vv0nD.js';
2
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
+
3
44
  type PromiseFactory<T, E> = () => Promise<Result<T, E>>;
4
- type AsyncOk<R> = R extends ResultAsync<infer T, unknown> ? T : never;
5
- type AsyncErr<R> = R extends ResultAsync<unknown, infer E> ? E : never;
6
45
  type CombineTupleAsync<R extends readonly ResultAsync<unknown, unknown>[]> = ResultAsync<{
7
- [K in keyof R]: AsyncOk<R[K]>;
46
+ [K in keyof R]: InferOk<R[K]>;
8
47
  }, {
9
- [K in keyof R]: AsyncErr<R[K]>;
48
+ [K in keyof R]: InferErr<R[K]>;
10
49
  }[number]>;
11
50
  /**
12
51
  * Async railway carrier. Wraps a deferred `Promise<Result<T, E>>` and exposes
@@ -92,6 +131,12 @@ declare class ResultAsync<T, E> {
92
131
  */
93
132
  static ok<T>(value: T): ResultAsync<T, never>;
94
133
  static ok<T, E>(value: T): ResultAsync<T, E>;
134
+ /**
135
+ * Fantasy Land `pure` — alias of {@link ResultAsync.ok}. One lift name
136
+ * shared across the trio (`of` / `Maybe.of` / `ResultAsync.of`).
137
+ */
138
+ static of<T>(value: T): ResultAsync<T, never>;
139
+ static of<T, E>(value: T): ResultAsync<T, E>;
95
140
  /**
96
141
  * Lifts an error into an `Err` async result.
97
142
  *
@@ -103,12 +148,13 @@ declare class ResultAsync<T, E> {
103
148
  static err<T = never, E = unknown>(error: E): ResultAsync<T, E>;
104
149
  /**
105
150
  * Lifts an existing `Promise<Result<T, E>>` (e.g. from interop code) into a
106
- * {@link ResultAsync}, auto-flattening one level of double-wrapping. A thrown
107
- * defect is routed through `onDefect`, defaulting to {@link UnexpectedError}.
151
+ * {@link ResultAsync}. A thrown defect is routed through `onDefect`,
152
+ * defaulting to {@link UnexpectedError}. The `Ok` value passes through
153
+ * verbatim — even when it happens to be Result-shaped.
108
154
  *
109
155
  * @param promise - a promise that already yields a `Result`
110
156
  * @param onDefect - maps an unexpected throw to the `Err` channel
111
- * @see {@link fromAsync} from `@onrails/result/interop`
157
+ * @see {@link fromAsync} from `@onrails/result`
112
158
  */
113
159
  static fromResultPromise<T, E>(promise: Promise<Result<T, E>>, onDefect?: (error: unknown) => E | UnexpectedError): ResultAsync<T, E | UnexpectedError>;
114
160
  /**
@@ -171,7 +217,7 @@ declare class ResultAsync<T, E> {
171
217
  *
172
218
  * @example
173
219
  * ```ts
174
- * load(id).mapErr((e): AppError => ({ kind: "load", cause: e }));
220
+ * load(id).mapErr((e): AppError => ({ kind: "load", cause: e })); // ResultAsync<T, AppError>
175
221
  * ```
176
222
  */
177
223
  mapErr<F>(fn: (error: E) => F): ResultAsync<T, F>;
@@ -186,21 +232,17 @@ declare class ResultAsync<T, E> {
186
232
  * .flatMap((p) => validate(p)); // sync Result also accepted
187
233
  * ```
188
234
  */
189
- flatMap<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F> | {
190
- inner: Result<U, F>;
191
- }): ResultAsync<U, E | F>;
235
+ flatMap<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F>): ResultAsync<U, E | F>;
192
236
  /**
193
237
  * neverthrow-compat alias of {@link flatMap}. Kept as the documented compat
194
- * tier; prefer `flatMap` in new code.
238
+ * tier; prefer {@link flatMap} in new code.
195
239
  *
196
240
  * @example
197
241
  * ```ts
198
242
  * authenticate(req).andThen((user) => loadProfile(user.id));
199
243
  * ```
200
244
  */
201
- andThen<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F> | {
202
- inner: Result<U, F>;
203
- }): ResultAsync<U, E | F>;
245
+ andThen<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F>): ResultAsync<U, E | F>;
204
246
  /**
205
247
  * Error-channel bind — runs `fn` only on `Err`, swapping in a recovery
206
248
  * `ResultAsync` or sync `Result`. `Ok` passes through. The mirror of
@@ -208,13 +250,13 @@ declare class ResultAsync<T, E> {
208
250
  *
209
251
  * @example
210
252
  * ```ts
211
- * loadFromCache(id).recover(() => loadFromOrigin(id));
253
+ * loadFromCache(id).recover(() => loadFromOrigin(id)); // ResultAsync<T, F>
212
254
  * ```
213
255
  */
214
256
  recover<F>(fn: (error: E) => ResultAsync<T, F> | Result<T, F>): ResultAsync<T, F>;
215
257
  /**
216
258
  * neverthrow-compat alias of {@link recover}. Kept as the documented compat
217
- * tier; prefer `recover` in new code.
259
+ * tier; prefer {@link recover} in new code.
218
260
  *
219
261
  * @example
220
262
  * ```ts
@@ -287,4 +329,4 @@ declare class ResultAsync<T, E> {
287
329
  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>;
288
330
  }
289
331
 
290
- export { ResultAsync as R };
332
+ export { type InferErr as I, ResultAsync as R, type InferOk as a };
@@ -1,12 +1,51 @@
1
1
  import { R as Result, U as UnexpectedError } from './types-BQ9vv0nD.cjs';
2
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
+
3
44
  type PromiseFactory<T, E> = () => Promise<Result<T, E>>;
4
- type AsyncOk<R> = R extends ResultAsync<infer T, unknown> ? T : never;
5
- type AsyncErr<R> = R extends ResultAsync<unknown, infer E> ? E : never;
6
45
  type CombineTupleAsync<R extends readonly ResultAsync<unknown, unknown>[]> = ResultAsync<{
7
- [K in keyof R]: AsyncOk<R[K]>;
46
+ [K in keyof R]: InferOk<R[K]>;
8
47
  }, {
9
- [K in keyof R]: AsyncErr<R[K]>;
48
+ [K in keyof R]: InferErr<R[K]>;
10
49
  }[number]>;
11
50
  /**
12
51
  * Async railway carrier. Wraps a deferred `Promise<Result<T, E>>` and exposes
@@ -92,6 +131,12 @@ declare class ResultAsync<T, E> {
92
131
  */
93
132
  static ok<T>(value: T): ResultAsync<T, never>;
94
133
  static ok<T, E>(value: T): ResultAsync<T, E>;
134
+ /**
135
+ * Fantasy Land `pure` — alias of {@link ResultAsync.ok}. One lift name
136
+ * shared across the trio (`of` / `Maybe.of` / `ResultAsync.of`).
137
+ */
138
+ static of<T>(value: T): ResultAsync<T, never>;
139
+ static of<T, E>(value: T): ResultAsync<T, E>;
95
140
  /**
96
141
  * Lifts an error into an `Err` async result.
97
142
  *
@@ -103,12 +148,13 @@ declare class ResultAsync<T, E> {
103
148
  static err<T = never, E = unknown>(error: E): ResultAsync<T, E>;
104
149
  /**
105
150
  * Lifts an existing `Promise<Result<T, E>>` (e.g. from interop code) into a
106
- * {@link ResultAsync}, auto-flattening one level of double-wrapping. A thrown
107
- * defect is routed through `onDefect`, defaulting to {@link UnexpectedError}.
151
+ * {@link ResultAsync}. A thrown defect is routed through `onDefect`,
152
+ * defaulting to {@link UnexpectedError}. The `Ok` value passes through
153
+ * verbatim — even when it happens to be Result-shaped.
108
154
  *
109
155
  * @param promise - a promise that already yields a `Result`
110
156
  * @param onDefect - maps an unexpected throw to the `Err` channel
111
- * @see {@link fromAsync} from `@onrails/result/interop`
157
+ * @see {@link fromAsync} from `@onrails/result`
112
158
  */
113
159
  static fromResultPromise<T, E>(promise: Promise<Result<T, E>>, onDefect?: (error: unknown) => E | UnexpectedError): ResultAsync<T, E | UnexpectedError>;
114
160
  /**
@@ -171,7 +217,7 @@ declare class ResultAsync<T, E> {
171
217
  *
172
218
  * @example
173
219
  * ```ts
174
- * load(id).mapErr((e): AppError => ({ kind: "load", cause: e }));
220
+ * load(id).mapErr((e): AppError => ({ kind: "load", cause: e })); // ResultAsync<T, AppError>
175
221
  * ```
176
222
  */
177
223
  mapErr<F>(fn: (error: E) => F): ResultAsync<T, F>;
@@ -186,21 +232,17 @@ declare class ResultAsync<T, E> {
186
232
  * .flatMap((p) => validate(p)); // sync Result also accepted
187
233
  * ```
188
234
  */
189
- flatMap<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F> | {
190
- inner: Result<U, F>;
191
- }): ResultAsync<U, E | F>;
235
+ flatMap<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F>): ResultAsync<U, E | F>;
192
236
  /**
193
237
  * neverthrow-compat alias of {@link flatMap}. Kept as the documented compat
194
- * tier; prefer `flatMap` in new code.
238
+ * tier; prefer {@link flatMap} in new code.
195
239
  *
196
240
  * @example
197
241
  * ```ts
198
242
  * authenticate(req).andThen((user) => loadProfile(user.id));
199
243
  * ```
200
244
  */
201
- andThen<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F> | {
202
- inner: Result<U, F>;
203
- }): ResultAsync<U, E | F>;
245
+ andThen<U, F = E>(fn: (value: T) => ResultAsync<U, F> | Result<U, F>): ResultAsync<U, E | F>;
204
246
  /**
205
247
  * Error-channel bind — runs `fn` only on `Err`, swapping in a recovery
206
248
  * `ResultAsync` or sync `Result`. `Ok` passes through. The mirror of
@@ -208,13 +250,13 @@ declare class ResultAsync<T, E> {
208
250
  *
209
251
  * @example
210
252
  * ```ts
211
- * loadFromCache(id).recover(() => loadFromOrigin(id));
253
+ * loadFromCache(id).recover(() => loadFromOrigin(id)); // ResultAsync<T, F>
212
254
  * ```
213
255
  */
214
256
  recover<F>(fn: (error: E) => ResultAsync<T, F> | Result<T, F>): ResultAsync<T, F>;
215
257
  /**
216
258
  * neverthrow-compat alias of {@link recover}. Kept as the documented compat
217
- * tier; prefer `recover` in new code.
259
+ * tier; prefer {@link recover} in new code.
218
260
  *
219
261
  * @example
220
262
  * ```ts
@@ -287,4 +329,4 @@ declare class ResultAsync<T, E> {
287
329
  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>;
288
330
  }
289
331
 
290
- export { ResultAsync as R };
332
+ export { type InferErr as I, ResultAsync as R, type InferOk as a };