unthrown 0.3.0 → 1.1.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/dist/index.mjs CHANGED
@@ -32,7 +32,7 @@ var UnwrapError = class extends Error {
32
32
  */
33
33
  var Res = class {
34
34
  map(f) {
35
- if (this.tag !== "Ok") return this;
35
+ if (this.tag !== "Ok") return passThrough(this);
36
36
  try {
37
37
  return okRes(f(this.value));
38
38
  } catch (cause) {
@@ -40,7 +40,7 @@ var Res = class {
40
40
  }
41
41
  }
42
42
  flatMap(f) {
43
- if (this.tag !== "Ok") return this;
43
+ if (this.tag !== "Ok") return passThrough(this);
44
44
  try {
45
45
  return f(this.value);
46
46
  } catch (cause) {
@@ -60,17 +60,41 @@ var Res = class {
60
60
  if (this.tag !== "Ok") return this;
61
61
  try {
62
62
  const r = f(this.value);
63
- return r.tag === "Ok" ? this : r;
63
+ return r.tag === "Ok" ? this : passThrough(r);
64
+ } catch (cause) {
65
+ return defectRes(cause);
66
+ }
67
+ }
68
+ bind(name, f) {
69
+ if (this.tag !== "Ok") return passThrough(this);
70
+ try {
71
+ const r = f(this.value);
72
+ if (r.tag !== "Ok") return passThrough(r);
73
+ return okRes({
74
+ ...scopeOf(this.value),
75
+ [name]: r.value
76
+ });
77
+ } catch (cause) {
78
+ return defectRes(cause);
79
+ }
80
+ }
81
+ let(name, f) {
82
+ if (this.tag !== "Ok") return passThrough(this);
83
+ try {
84
+ return okRes({
85
+ ...scopeOf(this.value),
86
+ [name]: f(this.value)
87
+ });
64
88
  } catch (cause) {
65
89
  return defectRes(cause);
66
90
  }
67
91
  }
68
92
  as(value) {
69
- if (this.tag !== "Ok") return this;
93
+ if (this.tag !== "Ok") return passThrough(this);
70
94
  return okRes(value);
71
95
  }
72
96
  mapErr(f) {
73
- if (this.tag !== "Err") return this;
97
+ if (this.tag !== "Err") return passThrough(this);
74
98
  try {
75
99
  return errRes(f(this.error));
76
100
  } catch (cause) {
@@ -78,7 +102,7 @@ var Res = class {
78
102
  }
79
103
  }
80
104
  orElse(f) {
81
- if (this.tag !== "Err") return this;
105
+ if (this.tag !== "Err") return passThrough(this);
82
106
  try {
83
107
  return f(this.error);
84
108
  } catch (cause) {
@@ -86,7 +110,7 @@ var Res = class {
86
110
  }
87
111
  }
88
112
  recover(f) {
89
- if (this.tag !== "Err") return this;
113
+ if (this.tag !== "Err") return passThrough(this);
90
114
  try {
91
115
  return okRes(f(this.error));
92
116
  } catch (cause) {
@@ -102,6 +126,15 @@ var Res = class {
102
126
  return defectRes(cause);
103
127
  }
104
128
  }
129
+ flatTapErr(f) {
130
+ if (this.tag !== "Err") return this;
131
+ try {
132
+ const r = f(this.error);
133
+ return r.tag === "Ok" ? this : passThrough(r);
134
+ } catch (cause) {
135
+ return defectRes(cause);
136
+ }
137
+ }
105
138
  recoverDefect(f) {
106
139
  if (this.tag !== "Defect") return this;
107
140
  try {
@@ -207,6 +240,56 @@ function defectRes(cause) {
207
240
  });
208
241
  }
209
242
  /**
243
+ * Type guard: is `x` a {@link Result} (any of `Ok` / `Err` / `Defect`)?
244
+ *
245
+ * @remarks
246
+ * Unlike {@link isOk} / {@link isErr} / {@link isDefect}, which narrow a value
247
+ * already known to be a `Result`, this narrows from `unknown` — useful at an
248
+ * untyped boundary. It checks the value carries the `Result` prototype, so a
249
+ * look-alike plain object (`{ tag: "Ok" }`) is **not** matched. An `AsyncResult`
250
+ * is not a `Result` and returns `false`.
251
+ *
252
+ * @returns `true` when `x` is a `Result` produced by this library.
253
+ */
254
+ function isResult(x) {
255
+ return x instanceof Res;
256
+ }
257
+ /**
258
+ * Reuse a non-matching variant (an `Err` or `Defect`) as a differently-typed
259
+ * `Result`, with no runtime work. Sound because the passed-through variant
260
+ * carries no value of the changed success type, so retyping it is a no-op — only
261
+ * the phantom type parameter moves. This is the single sanctioned home for that
262
+ * assertion (the same one boxed applies inline at every pass-through); every
263
+ * combinator's short-circuit branch funnels through here instead of casting.
264
+ *
265
+ * @internal
266
+ */
267
+ function passThrough(self) {
268
+ return self;
269
+ }
270
+ /**
271
+ * Validate that a `bind`/`let` scope is a real (non-null) object before merging a
272
+ * key into it.
273
+ *
274
+ * @remarks
275
+ * Do-notation accumulates an **object** scope: a chain starts at `Do()` (an
276
+ * empty object) and every `bind`/`let` returns an object, so in typed code the
277
+ * scope is always an object. The method lives on the general `Result` surface,
278
+ * though, so a primitive `Ok` (e.g. `Ok(5).bind(...)`, or a chain whose value was
279
+ * `map`-ped away from its scope) could reach it. Rather than let `{ ...5 }`
280
+ * silently collapse to `{}` and drop the prior scope, we throw here — the
281
+ * surrounding `try` turns it into a {@link Defect}, surfacing the misuse as the
282
+ * bug it is (a defect is a bug, not an absent value). A `this: object` constraint
283
+ * was rejected: TypeScript does not hard-enforce a constraint inferred solely
284
+ * from `this`, and it breaks `AsyncRes implements AsyncResult`.
285
+ *
286
+ * @internal
287
+ */
288
+ function scopeOf(value) {
289
+ if (typeof value !== "object" || value === null) throw new TypeError("bind/let requires an object scope — start a do-chain with Do()");
290
+ return value;
291
+ }
292
+ /**
210
293
  * The sole runtime implementation of {@link AsyncResult}: wraps a
211
294
  * `Promise<Result>` constructed never to reject. Operates on the public `Result`
212
295
  * union (via `tag`), never on `Res` internals. Never re-exported from `index.ts`.
@@ -223,7 +306,7 @@ var AsyncRes = class AsyncRes {
223
306
  }
224
307
  map(f) {
225
308
  return new AsyncRes(this.promise.then((r) => {
226
- if (r.tag !== "Ok") return r;
309
+ if (r.tag !== "Ok") return passThrough(r);
227
310
  try {
228
311
  return okRes(f(r.value));
229
312
  } catch (cause) {
@@ -233,7 +316,7 @@ var AsyncRes = class AsyncRes {
233
316
  }
234
317
  flatMap(f) {
235
318
  return new AsyncRes(this.promise.then(async (r) => {
236
- if (r.tag !== "Ok") return r;
319
+ if (r.tag !== "Ok") return passThrough(r);
237
320
  try {
238
321
  return await f(r.value);
239
322
  } catch (cause) {
@@ -254,21 +337,49 @@ var AsyncRes = class AsyncRes {
254
337
  }
255
338
  flatTap(f) {
256
339
  return new AsyncRes(this.promise.then(async (r) => {
257
- if (r.tag !== "Ok") return r;
340
+ if (r.tag !== "Ok") return passThrough(r);
341
+ try {
342
+ const inner = await f(r.value);
343
+ return inner.tag === "Ok" ? r : passThrough(inner);
344
+ } catch (cause) {
345
+ return defectRes(cause);
346
+ }
347
+ }));
348
+ }
349
+ bind(name, f) {
350
+ return new AsyncRes(this.promise.then(async (r) => {
351
+ if (r.tag !== "Ok") return passThrough(r);
258
352
  try {
259
353
  const inner = await f(r.value);
260
- return inner.tag === "Ok" ? r : inner;
354
+ if (inner.tag !== "Ok") return passThrough(inner);
355
+ return okRes({
356
+ ...scopeOf(r.value),
357
+ [name]: inner.value
358
+ });
359
+ } catch (cause) {
360
+ return defectRes(cause);
361
+ }
362
+ }));
363
+ }
364
+ let(name, f) {
365
+ return new AsyncRes(this.promise.then((r) => {
366
+ if (r.tag !== "Ok") return passThrough(r);
367
+ try {
368
+ return okRes({
369
+ ...scopeOf(r.value),
370
+ [name]: f(r.value)
371
+ });
261
372
  } catch (cause) {
262
373
  return defectRes(cause);
263
374
  }
264
375
  }));
265
376
  }
266
377
  as(value) {
267
- return new AsyncRes(this.promise.then((r) => r.tag === "Ok" ? okRes(value) : r));
378
+ return new AsyncRes(this.promise.then((r) => r.tag === "Ok" ? okRes(value) : passThrough(r)));
268
379
  }
269
380
  mapErr(f) {
270
381
  return new AsyncRes(this.promise.then((r) => {
271
- if (r.tag !== "Err") return r;
382
+ if (r.tag !== "Err") return passThrough(r);
272
383
  try {
273
384
  return errRes(f(r.error));
274
385
  } catch (cause) {
@@ -278,7 +389,7 @@ var AsyncRes = class AsyncRes {
278
389
  }
279
390
  orElse(f) {
280
391
  return new AsyncRes(this.promise.then(async (r) => {
281
- if (r.tag !== "Err") return r;
392
+ if (r.tag !== "Err") return passThrough(r);
282
393
  try {
283
394
  return await f(r.error);
284
395
  } catch (cause) {
@@ -288,7 +399,7 @@ var AsyncRes = class AsyncRes {
288
399
  }
289
400
  recover(f) {
290
401
  return new AsyncRes(this.promise.then((r) => {
291
- if (r.tag !== "Err") return r;
402
+ if (r.tag !== "Err") return passThrough(r);
292
403
  try {
293
404
  return okRes(f(r.error));
294
405
  } catch (cause) {
@@ -307,6 +418,17 @@ var AsyncRes = class AsyncRes {
307
418
  }
308
419
  }));
309
420
  }
421
+ flatTapErr(f) {
422
+ return new AsyncRes(this.promise.then(async (r) => {
423
+ if (r.tag !== "Err") return passThrough(r);
424
+ try {
425
+ const inner = await f(r.error);
426
+ return inner.tag === "Ok" ? passThrough(r) : passThrough(inner);
427
+ } catch (cause) {
428
+ return defectRes(cause);
429
+ }
430
+ }));
431
+ }
310
432
  recoverDefect(f) {
311
433
  return new AsyncRes(this.promise.then(async (r) => {
312
434
  if (r.tag !== "Defect") return r;
@@ -364,11 +486,11 @@ var AsyncRes = class AsyncRes {
364
486
  *
365
487
  * @example
366
488
  * ```ts
367
- * import { ok } from "unthrown";
368
- * ok(42).unwrap(); // 42
489
+ * import { Ok } from "unthrown";
490
+ * Ok(42).unwrap(); // 42
369
491
  * ```
370
492
  */
371
- function ok(value) {
493
+ function Ok(value) {
372
494
  return okRes(value);
373
495
  }
374
496
  /**
@@ -379,11 +501,11 @@ function ok(value) {
379
501
  *
380
502
  * @example
381
503
  * ```ts
382
- * import { err } from "unthrown";
383
- * err("not_found").unwrapErr(); // "not_found"
504
+ * import { Err } from "unthrown";
505
+ * Err("not_found").unwrapErr(); // "not_found"
384
506
  * ```
385
507
  */
386
- function err(error) {
508
+ function Err(error) {
387
509
  return errRes(error);
388
510
  }
389
511
  /**
@@ -419,24 +541,24 @@ function isDefect(r) {
419
541
  }
420
542
  //#endregion
421
543
  //#region src/defect.ts
422
- const DEFECT = Symbol("unthrown/defect");
544
+ const DEFECT = Symbol("unthrown/Defect");
423
545
  /**
424
546
  * Wrap a cause as a {@link Defect} — the value you return from a `qualify`
425
547
  * function when a failure is **not** a modeled domain error.
426
548
  *
427
549
  * @param cause - the original thrown/rejected value.
428
- * @returns an opaque defect marker carrying `cause`.
550
+ * @returns an opaque Defect marker carrying `cause`.
429
551
  *
430
552
  * @example
431
553
  * ```ts
432
- * import { fromPromise, defect } from "unthrown";
554
+ * import { fromPromise, Defect } from "unthrown";
433
555
  *
434
556
  * const user = fromPromise(fetchUser(id), (cause) =>
435
- * cause instanceof NotFoundError ? cause : defect(cause),
557
+ * cause instanceof NotFoundError ? cause : Defect(cause),
436
558
  * );
437
559
  * ```
438
560
  */
439
- function defect(cause) {
561
+ function Defect(cause) {
440
562
  return {
441
563
  [DEFECT]: true,
442
564
  cause
@@ -453,13 +575,40 @@ function isDefectMarker(x) {
453
575
  return typeof x === "object" && x !== null && x[DEFECT] === true;
454
576
  }
455
577
  //#endregion
578
+ //#region src/do.ts
579
+ /**
580
+ * Start a do-notation chain with an empty object scope, grown step by step with
581
+ * `bind` (for `Result`-returning steps) and `let` (for pure values).
582
+ *
583
+ * @remarks
584
+ * Capitalised because `do` is a reserved word. Each step receives the scope
585
+ * accumulated so far; the error types union across `bind`s, and a throw in any
586
+ * step becomes a `Defect`. To go asynchronous, lift the chain with `toAsync()`
587
+ * (then a `bind` may return an `AsyncResult`).
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * import { Do, Ok } from "unthrown";
592
+ *
593
+ * const result = Do()
594
+ * .bind("user", () => findUser(id)) // Result<User, NotFound>
595
+ * .bind("org", ({ user }) => findOrg(user.orgId)) // Result<Org, NotFound>
596
+ * .let("label", ({ user, org }) => `${user.name} @ ${org.name}`)
597
+ * .map(({ user, org, label }) => render(user, org, label));
598
+ * // Result<View, NotFound>
599
+ * ```
600
+ */
601
+ function Do() {
602
+ return Ok({});
603
+ }
604
+ //#endregion
456
605
  //#region src/interop.ts
457
606
  /**
458
607
  * Bridge a nullable value into a {@link Result}: absence becomes a **modeled**
459
608
  * `Err`. The sanctioned alternative to an `Option` type.
460
609
  *
461
610
  * @remarks
462
- * `null` and `undefined` map to `err(onAbsent())`; any other value (including
611
+ * `null` and `undefined` map to `Err(onAbsent())`; any other value (including
463
612
  * falsy ones like `0`, `""`, `false`) maps to `Ok`.
464
613
  *
465
614
  * @typeParam T - the (nullable) value type.
@@ -474,7 +623,7 @@ function isDefectMarker(x) {
474
623
  * ```
475
624
  */
476
625
  function fromNullable(value, onAbsent) {
477
- return value === null || value === void 0 ? err(onAbsent()) : ok(value);
626
+ return value === null || value === void 0 ? Err(onAbsent()) : Ok(value);
478
627
  }
479
628
  /**
480
629
  * Wrap a throwing synchronous function so it returns a {@link Result} instead of
@@ -482,14 +631,14 @@ function fromNullable(value, onAbsent) {
482
631
  *
483
632
  * @remarks
484
633
  * `qualify` **must** triage every thrown cause into a modeled error `E` or a
485
- * {@link Defect} (via {@link defect}) — there is no path that leaves `unknown`
634
+ * {@link Defect} (via {@link Defect}) — there is no path that leaves `unknown`
486
635
  * in `E`. A throw inside `qualify` itself is treated as a `Defect`.
487
636
  *
488
637
  * The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
489
638
  * `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
490
- * `qualify` that returns *only* `defect(cause)` yields `E = never` (a defect is
639
+ * `qualify` that returns *only* `Defect(cause)` yields `E = never` (a Defect is
491
640
  * out-of-band and must not pollute the error channel); reach for
492
- * {@link fromSafePromise} when every failure is a defect.
641
+ * {@link fromSafePromise} when every failure is a Defect.
493
642
  *
494
643
  * @typeParam A - the wrapped function's argument tuple.
495
644
  * @typeParam T - the wrapped function's return type.
@@ -501,8 +650,8 @@ function fromNullable(value, onAbsent) {
501
650
  *
502
651
  * @example
503
652
  * ```ts
504
- * import { fromThrowable, defect } from "unthrown";
505
- * const parse = fromThrowable(JSON.parse, (cause) => defect(cause));
653
+ * import { fromThrowable, Defect } from "unthrown";
654
+ * const parse = fromThrowable(JSON.parse, (cause) => Defect(cause));
506
655
  * parse("{}").unwrap();
507
656
  * ```
508
657
  */
@@ -510,7 +659,7 @@ function fromThrowable(fn, qualify) {
510
659
  const triage = qualify;
511
660
  return (...args) => {
512
661
  try {
513
- return ok(fn(...args));
662
+ return Ok(fn(...args));
514
663
  } catch (cause) {
515
664
  return qualifyToResult(cause, triage);
516
665
  }
@@ -528,8 +677,8 @@ function fromThrowable(fn, qualify) {
528
677
  *
529
678
  * The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
530
679
  * `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
531
- * `qualify` that returns *only* `defect(cause)` yields `E = never`; when every
532
- * rejection is a defect, prefer {@link fromSafePromise}.
680
+ * `qualify` that returns *only* `Defect(cause)` yields `E = never`; when every
681
+ * rejection is a Defect, prefer {@link fromSafePromise}.
533
682
  *
534
683
  * @typeParam T - the resolved value type.
535
684
  * @typeParam R - `qualify`'s return type; the modeled error `E` is
@@ -539,9 +688,9 @@ function fromThrowable(fn, qualify) {
539
688
  *
540
689
  * @example
541
690
  * ```ts
542
- * import { fromPromise, defect } from "unthrown";
691
+ * import { fromPromise, Defect } from "unthrown";
543
692
  * const user = await fromPromise(fetchUser(id), (cause) =>
544
- * cause instanceof NotFoundError ? ("not_found" as const) : defect(cause),
693
+ * cause instanceof NotFoundError ? ("not_found" as const) : Defect(cause),
545
694
  * );
546
695
  * ```
547
696
  */
@@ -585,7 +734,7 @@ function foldArray(results) {
585
734
  for (const r of results) if (r.tag === "Defect") firstDefect ??= r;
586
735
  else if (r.tag === "Err") firstErr ??= r;
587
736
  else values.push(r.value);
588
- return firstDefect ?? firstErr ?? ok(values);
737
+ return firstDefect ?? firstErr ?? Ok(values);
589
738
  }
590
739
  /**
591
740
  * Fold a record of settled `Result`s with the same rules, else `Ok` of the
@@ -606,7 +755,7 @@ function foldRecord(results) {
606
755
  writable: true,
607
756
  configurable: true
608
757
  });
609
- return firstDefect ?? firstErr ?? ok(values);
758
+ return firstDefect ?? firstErr ?? Ok(values);
610
759
  }
611
760
  /**
612
761
  * Collect a tuple/array of {@link Result}s into a single `Result` of all their
@@ -615,16 +764,16 @@ function foldRecord(results) {
615
764
  * @remarks
616
765
  * Short-circuits on the **first** `Err` (later entries are not inspected for
617
766
  * their error); any `Defect` present **dominates**, winning even over an earlier
618
- * `Err`. A **fixed tuple** keeps its positional types — `all([ok(1), ok("a")])`
767
+ * `Err`. A **fixed tuple** keeps its positional types — `all([Ok(1), Ok("a")])`
619
768
  * is `Result<[number, string], …>` — while a **dynamic array** `Result<T, E>[]`
620
769
  * collapses to `Result<T[], E>` with no cast. For a **record** keyed by name,
621
770
  * use {@link allFromDict}.
622
771
  *
623
772
  * @example
624
773
  * ```ts
625
- * import { all, ok } from "unthrown";
626
- * all([ok(1), ok("a"), ok(true)]).unwrap(); // [1, "a", true] (typed [number, string, boolean])
627
- * all([ok(1), ok(2)] as Result<number, never>[]).unwrap(); // number[]
774
+ * import { all, Ok } from "unthrown";
775
+ * all([Ok(1), Ok("a"), Ok(true)]).unwrap(); // [1, "a", true] (typed [number, string, boolean])
776
+ * all([Ok(1), Ok(2)] as Result<number, never>[]).unwrap(); // number[]
628
777
  * ```
629
778
  */
630
779
  function all(results) {
@@ -642,8 +791,8 @@ function all(results) {
642
791
  *
643
792
  * @example
644
793
  * ```ts
645
- * import { allFromDict, ok } from "unthrown";
646
- * allFromDict({ id: ok(1), name: ok("ada") }).unwrap(); // { id: 1, name: "ada" }
794
+ * import { allFromDict, Ok } from "unthrown";
795
+ * allFromDict({ id: Ok(1), name: Ok("ada") }).unwrap(); // { id: 1, name: "ada" }
647
796
  * ```
648
797
  */
649
798
  function allFromDict(results) {
@@ -696,29 +845,30 @@ function allFromDictAsync(results) {
696
845
  //#region src/facade.ts
697
846
  /**
698
847
  * Companion object grouping the standalone entry points under a single,
699
- * discoverable namespace: {@link Result.ok}, {@link Result.err},
700
- * {@link Result.defect}, {@link Result.fromNullable}, {@link Result.fromThrowable},
848
+ * discoverable namespace: {@link Result.Ok}, {@link Result.Err},
849
+ * {@link Result.Defect}, {@link Result.fromNullable}, {@link Result.fromThrowable},
701
850
  * {@link Result.fromPromise}, {@link Result.fromSafePromise}, {@link Result.all},
702
851
  * {@link Result.allAsync}, {@link Result.allFromDict},
703
852
  * {@link Result.allFromDictAsync}, {@link Result.isOk}, {@link Result.isErr},
704
- * {@link Result.isDefect}.
853
+ * {@link Result.isDefect}, {@link Result.isResult}.
705
854
  *
706
855
  * @remarks
707
856
  * Purely additive sugar — each member **is** the corresponding free function.
708
857
  * The free functions remain the primary, tree-shakeable API; importing only
709
- * `{ ok }` never pulls this object in. The value `Result` and the type
858
+ * `{ Ok }` never pulls this object in. The value `Result` and the type
710
859
  * {@link Result} share one name (the companion-object pattern).
711
860
  *
712
861
  * @example
713
862
  * ```ts
714
863
  * import { Result } from "unthrown";
715
- * Result.ok(1).flatMap((n) => Result.ok(n + 1)).unwrap(); // 2
864
+ * Result.Ok(1).flatMap((n) => Result.Ok(n + 1)).unwrap(); // 2
716
865
  * ```
717
866
  */
718
867
  const Result = {
719
- ok,
720
- err,
721
- defect,
868
+ Ok,
869
+ Err,
870
+ Defect,
871
+ Do,
722
872
  fromNullable,
723
873
  fromThrowable,
724
874
  fromPromise,
@@ -729,7 +879,8 @@ const Result = {
729
879
  allFromDictAsync,
730
880
  isOk,
731
881
  isErr,
732
- isDefect
882
+ isDefect,
883
+ isResult
733
884
  };
734
885
  //#endregion
735
886
  //#region src/tagged.ts
@@ -799,6 +950,6 @@ function matchTags(result, handlers) {
799
950
  });
800
951
  }
801
952
  //#endregion
802
- export { Result, TaggedError, UnwrapError, all, allAsync, allFromDict, allFromDictAsync, defect, err, fromNullable, fromPromise, fromSafePromise, fromThrowable, isDefect, isErr, isOk, matchTags, ok };
953
+ export { Defect, Do, Err, Ok, Result, TaggedError, UnwrapError, all, allAsync, allFromDict, allFromDictAsync, fromNullable, fromPromise, fromSafePromise, fromThrowable, isDefect, isErr, isOk, isResult, matchTags };
803
954
 
804
955
  //# sourceMappingURL=index.mjs.map