unthrown 0.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.cjs ADDED
@@ -0,0 +1,668 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/core.ts
3
+ /**
4
+ * Thrown by a {@link Result}'s `unwrap` / `unwrapErr` when the assertion is
5
+ * wrong on a *modeled* result — `unwrap()` on an `Err`, or `unwrapErr()` on an
6
+ * `Ok`.
7
+ *
8
+ * @remarks
9
+ * A `Defect` is never wrapped in an `UnwrapError`: its original cause is
10
+ * re-thrown (with its original stack) instead.
11
+ *
12
+ * @typeParam E - the type of the {@link UnwrapError.error} it carries.
13
+ */
14
+ var UnwrapError = class extends Error {
15
+ /**
16
+ * The offending value: the `Err` error for `unwrap()`, or the `Ok` value for
17
+ * `unwrapErr()`.
18
+ */
19
+ error;
20
+ constructor(error) {
21
+ super("unthrown: called unwrap on a non-matching Result");
22
+ this.name = "UnwrapError";
23
+ this.error = error;
24
+ Object.setPrototypeOf(this, new.target.prototype);
25
+ }
26
+ };
27
+ /**
28
+ * Method holder for {@link Result}. Never instantiated with `new` and never
29
+ * exported; the builders below attach its prototype to plain objects. Every
30
+ * method types `this` as the public `Result` union, so it narrows on `tag`.
31
+ *
32
+ * @internal
33
+ */
34
+ var Res = class {
35
+ map(f) {
36
+ if (this.tag !== "Ok") return this;
37
+ try {
38
+ return okRes(f(this.value));
39
+ } catch (cause) {
40
+ return defectRes(cause);
41
+ }
42
+ }
43
+ flatMap(f) {
44
+ if (this.tag !== "Ok") return this;
45
+ try {
46
+ return f(this.value);
47
+ } catch (cause) {
48
+ return defectRes(cause);
49
+ }
50
+ }
51
+ tap(f) {
52
+ if (this.tag !== "Ok") return this;
53
+ try {
54
+ f(this.value);
55
+ return this;
56
+ } catch (cause) {
57
+ return defectRes(cause);
58
+ }
59
+ }
60
+ as(value) {
61
+ if (this.tag !== "Ok") return this;
62
+ return okRes(value);
63
+ }
64
+ mapErr(f) {
65
+ if (this.tag !== "Err") return this;
66
+ try {
67
+ return errRes(f(this.error));
68
+ } catch (cause) {
69
+ return defectRes(cause);
70
+ }
71
+ }
72
+ orElse(f) {
73
+ if (this.tag !== "Err") return this;
74
+ try {
75
+ return f(this.error);
76
+ } catch (cause) {
77
+ return defectRes(cause);
78
+ }
79
+ }
80
+ recover(f) {
81
+ if (this.tag !== "Err") return this;
82
+ try {
83
+ return okRes(f(this.error));
84
+ } catch (cause) {
85
+ return defectRes(cause);
86
+ }
87
+ }
88
+ tapErr(f) {
89
+ if (this.tag !== "Err") return this;
90
+ try {
91
+ f(this.error);
92
+ return this;
93
+ } catch (cause) {
94
+ return defectRes(cause);
95
+ }
96
+ }
97
+ recoverDefect(f) {
98
+ if (this.tag !== "Defect") return this;
99
+ try {
100
+ return f(this.cause);
101
+ } catch (cause) {
102
+ return defectRes(cause);
103
+ }
104
+ }
105
+ tapDefect(f) {
106
+ if (this.tag !== "Defect") return this;
107
+ try {
108
+ f(this.cause);
109
+ return this;
110
+ } catch (cause) {
111
+ return defectRes(cause);
112
+ }
113
+ }
114
+ match(cases) {
115
+ switch (this.tag) {
116
+ case "Ok": return cases.ok(this.value);
117
+ case "Err": return cases.err(this.error);
118
+ case "Defect": return cases.defect(this.cause);
119
+ }
120
+ }
121
+ unwrap() {
122
+ switch (this.tag) {
123
+ case "Ok": return this.value;
124
+ case "Err": throw new UnwrapError(this.error);
125
+ case "Defect": throw this.cause;
126
+ }
127
+ }
128
+ unwrapErr() {
129
+ switch (this.tag) {
130
+ case "Err": return this.error;
131
+ case "Ok": throw new UnwrapError(this.value);
132
+ case "Defect": throw this.cause;
133
+ }
134
+ }
135
+ unwrapOr(fallback) {
136
+ if (this.tag === "Ok") return this.value;
137
+ if (this.tag === "Defect") throw this.cause;
138
+ return fallback;
139
+ }
140
+ unwrapOrElse(f) {
141
+ if (this.tag === "Ok") return this.value;
142
+ if (this.tag === "Defect") throw this.cause;
143
+ return f(this.error);
144
+ }
145
+ getOrNull() {
146
+ if (this.tag === "Ok") return this.value;
147
+ if (this.tag === "Defect") throw this.cause;
148
+ return null;
149
+ }
150
+ getOrUndefined() {
151
+ if (this.tag === "Ok") return this.value;
152
+ if (this.tag === "Defect") throw this.cause;
153
+ }
154
+ isOk() {
155
+ return this.tag === "Ok";
156
+ }
157
+ isErr() {
158
+ return this.tag === "Err";
159
+ }
160
+ isDefect() {
161
+ return this.tag === "Defect";
162
+ }
163
+ toAsync() {
164
+ return new AsyncRes(Promise.resolve(this));
165
+ }
166
+ };
167
+ const RESULT_PROTO = Res.prototype;
168
+ /**
169
+ * Construct an `Ok` result — a plain object on the {@link Res} prototype.
170
+ *
171
+ * @internal
172
+ */
173
+ function okRes(value) {
174
+ return Object.assign(Object.create(RESULT_PROTO), {
175
+ tag: "Ok",
176
+ value
177
+ });
178
+ }
179
+ /**
180
+ * Construct an `Err` result.
181
+ *
182
+ * @internal
183
+ */
184
+ function errRes(error) {
185
+ return Object.assign(Object.create(RESULT_PROTO), {
186
+ tag: "Err",
187
+ error
188
+ });
189
+ }
190
+ /**
191
+ * Construct a `Defect` result.
192
+ *
193
+ * @internal
194
+ */
195
+ function defectRes(cause) {
196
+ return Object.assign(Object.create(RESULT_PROTO), {
197
+ tag: "Defect",
198
+ cause
199
+ });
200
+ }
201
+ /**
202
+ * The sole runtime implementation of {@link AsyncResult}: wraps a
203
+ * `Promise<Result>` constructed never to reject. Operates on the public `Result`
204
+ * union (via `tag`), never on `Res` internals. Never re-exported from `index.ts`.
205
+ *
206
+ * @internal
207
+ */
208
+ var AsyncRes = class AsyncRes {
209
+ promise;
210
+ constructor(promise) {
211
+ this.promise = promise;
212
+ }
213
+ then(onfulfilled, onrejected) {
214
+ return this.promise.then(onfulfilled, onrejected);
215
+ }
216
+ map(f) {
217
+ return new AsyncRes(this.promise.then((r) => {
218
+ if (r.tag !== "Ok") return r;
219
+ try {
220
+ return okRes(f(r.value));
221
+ } catch (cause) {
222
+ return defectRes(cause);
223
+ }
224
+ }));
225
+ }
226
+ flatMap(f) {
227
+ return new AsyncRes(this.promise.then(async (r) => {
228
+ if (r.tag !== "Ok") return r;
229
+ try {
230
+ return await f(r.value);
231
+ } catch (cause) {
232
+ return defectRes(cause);
233
+ }
234
+ }));
235
+ }
236
+ tap(f) {
237
+ return new AsyncRes(this.promise.then((r) => {
238
+ if (r.tag !== "Ok") return r;
239
+ try {
240
+ f(r.value);
241
+ return r;
242
+ } catch (cause) {
243
+ return defectRes(cause);
244
+ }
245
+ }));
246
+ }
247
+ as(value) {
248
+ return new AsyncRes(this.promise.then((r) => r.tag === "Ok" ? okRes(value) : r));
249
+ }
250
+ mapErr(f) {
251
+ return new AsyncRes(this.promise.then((r) => {
252
+ if (r.tag !== "Err") return r;
253
+ try {
254
+ return errRes(f(r.error));
255
+ } catch (cause) {
256
+ return defectRes(cause);
257
+ }
258
+ }));
259
+ }
260
+ orElse(f) {
261
+ return new AsyncRes(this.promise.then(async (r) => {
262
+ if (r.tag !== "Err") return r;
263
+ try {
264
+ return await f(r.error);
265
+ } catch (cause) {
266
+ return defectRes(cause);
267
+ }
268
+ }));
269
+ }
270
+ recover(f) {
271
+ return new AsyncRes(this.promise.then((r) => {
272
+ if (r.tag !== "Err") return r;
273
+ try {
274
+ return okRes(f(r.error));
275
+ } catch (cause) {
276
+ return defectRes(cause);
277
+ }
278
+ }));
279
+ }
280
+ tapErr(f) {
281
+ return new AsyncRes(this.promise.then((r) => {
282
+ if (r.tag !== "Err") return r;
283
+ try {
284
+ f(r.error);
285
+ return r;
286
+ } catch (cause) {
287
+ return defectRes(cause);
288
+ }
289
+ }));
290
+ }
291
+ recoverDefect(f) {
292
+ return new AsyncRes(this.promise.then(async (r) => {
293
+ if (r.tag !== "Defect") return r;
294
+ try {
295
+ return await f(r.cause);
296
+ } catch (cause) {
297
+ return defectRes(cause);
298
+ }
299
+ }));
300
+ }
301
+ tapDefect(f) {
302
+ return new AsyncRes(this.promise.then((r) => {
303
+ if (r.tag !== "Defect") return r;
304
+ try {
305
+ f(r.cause);
306
+ return r;
307
+ } catch (cause) {
308
+ return defectRes(cause);
309
+ }
310
+ }));
311
+ }
312
+ match(cases) {
313
+ return this.promise.then((r) => r.match(cases));
314
+ }
315
+ unwrap() {
316
+ return this.promise.then((r) => r.unwrap());
317
+ }
318
+ unwrapErr() {
319
+ return this.promise.then((r) => r.unwrapErr());
320
+ }
321
+ unwrapOr(fallback) {
322
+ return this.promise.then((r) => r.unwrapOr(fallback));
323
+ }
324
+ unwrapOrElse(f) {
325
+ return this.promise.then((r) => {
326
+ if (r.tag === "Ok") return r.value;
327
+ if (r.tag === "Defect") throw r.cause;
328
+ return f(r.error);
329
+ });
330
+ }
331
+ getOrNull() {
332
+ return this.promise.then((r) => r.getOrNull());
333
+ }
334
+ getOrUndefined() {
335
+ return this.promise.then((r) => r.getOrUndefined());
336
+ }
337
+ };
338
+ //#endregion
339
+ //#region src/constructors.ts
340
+ /**
341
+ * Construct a successful {@link Result}.
342
+ *
343
+ * @typeParam T - the success value type.
344
+ * @param value - the success value to wrap.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * import { ok } from "unthrown";
349
+ * ok(42).unwrap(); // 42
350
+ * ```
351
+ */
352
+ function ok(value) {
353
+ return okRes(value);
354
+ }
355
+ /**
356
+ * Construct a failed {@link Result} carrying a **modeled** error.
357
+ *
358
+ * @typeParam E - the modeled error type.
359
+ * @param error - the domain error to wrap.
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * import { err } from "unthrown";
364
+ * err("not_found").unwrapErr(); // "not_found"
365
+ * ```
366
+ */
367
+ function err(error) {
368
+ return errRes(error);
369
+ }
370
+ /**
371
+ * Type guard: narrow a {@link Result} to its `Ok` variant, exposing `.value`.
372
+ *
373
+ * @returns `true` when `r` is `Ok`.
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * import { isOk, type Result } from "unthrown";
378
+ * declare const r: Result<number, string>;
379
+ * if (isOk(r)) r.value; // number, narrowed
380
+ * ```
381
+ */
382
+ function isOk(r) {
383
+ return r.tag === "Ok";
384
+ }
385
+ /**
386
+ * Type guard: narrow a {@link Result} to its `Err` variant, exposing `.error`.
387
+ *
388
+ * @returns `true` when `r` is `Err`.
389
+ */
390
+ function isErr(r) {
391
+ return r.tag === "Err";
392
+ }
393
+ /**
394
+ * Type guard: narrow a {@link Result} to its `Defect` variant, exposing `.cause`.
395
+ *
396
+ * @returns `true` when `r` is a `Defect`.
397
+ */
398
+ function isDefect(r) {
399
+ return r.tag === "Defect";
400
+ }
401
+ //#endregion
402
+ //#region src/defect.ts
403
+ const DEFECT = Symbol("unthrown/defect");
404
+ /**
405
+ * Wrap a cause as a {@link Defect} — the value you return from a `qualify`
406
+ * function when a failure is **not** a modeled domain error.
407
+ *
408
+ * @param cause - the original thrown/rejected value.
409
+ * @returns an opaque defect marker carrying `cause`.
410
+ *
411
+ * @example
412
+ * ```ts
413
+ * import { fromPromise, defect } from "unthrown";
414
+ *
415
+ * const user = fromPromise(fetchUser(id), (cause) =>
416
+ * cause instanceof NotFoundError ? cause : defect(cause),
417
+ * );
418
+ * ```
419
+ */
420
+ function defect(cause) {
421
+ return {
422
+ [DEFECT]: true,
423
+ cause
424
+ };
425
+ }
426
+ /**
427
+ * Internal guard for the qualify-time marker. Distinct from the public
428
+ * {@link isDefect} state guard — this one narrows the `E | Defect` union a
429
+ * `qualify` function returns, not a `Result`.
430
+ *
431
+ * @internal
432
+ */
433
+ function isDefectMarker(x) {
434
+ return typeof x === "object" && x !== null && x[DEFECT] === true;
435
+ }
436
+ //#endregion
437
+ //#region src/interop.ts
438
+ /**
439
+ * Bridge a nullable value into a {@link Result}: absence becomes a **modeled**
440
+ * `Err`. The sanctioned alternative to an `Option` type.
441
+ *
442
+ * @remarks
443
+ * `null` and `undefined` map to `err(onAbsent())`; any other value (including
444
+ * falsy ones like `0`, `""`, `false`) maps to `Ok`.
445
+ *
446
+ * @typeParam T - the (nullable) value type.
447
+ * @typeParam E - the error produced when the value is absent.
448
+ * @param value - the possibly-absent value.
449
+ * @param onAbsent - lazily produces the error for the absent case.
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * import { fromNullable } from "unthrown";
454
+ * fromNullable(map.get(key), () => "missing").unwrap();
455
+ * ```
456
+ */
457
+ function fromNullable(value, onAbsent) {
458
+ return value === null || value === void 0 ? err(onAbsent()) : ok(value);
459
+ }
460
+ /**
461
+ * Wrap a throwing synchronous function so it returns a {@link Result} instead of
462
+ * throwing.
463
+ *
464
+ * @remarks
465
+ * `qualify` **must** triage every thrown cause into a modeled error `E` or a
466
+ * {@link Defect} (via {@link defect}) — there is no path that leaves `unknown`
467
+ * in `E`. A throw inside `qualify` itself is treated as a `Defect`.
468
+ *
469
+ * @typeParam A - the wrapped function's argument tuple.
470
+ * @typeParam T - the wrapped function's return type.
471
+ * @typeParam E - the modeled error type.
472
+ * @param fn - the throwing function to wrap.
473
+ * @param qualify - triages a thrown cause into `E` or a `Defect`.
474
+ * @returns a function with the same arguments returning `Result<T, E>`.
475
+ *
476
+ * @example
477
+ * ```ts
478
+ * import { fromThrowable, defect } from "unthrown";
479
+ * const parse = fromThrowable(JSON.parse, (cause) => defect(cause));
480
+ * parse("{}").unwrap();
481
+ * ```
482
+ */
483
+ function fromThrowable(fn, qualify) {
484
+ return (...args) => {
485
+ try {
486
+ return ok(fn(...args));
487
+ } catch (cause) {
488
+ return qualifyToResult(cause, qualify);
489
+ }
490
+ };
491
+ }
492
+ /**
493
+ * Wrap a `Promise` (or a thunk producing one) as an {@link AsyncResult}, forcing
494
+ * every rejection to be triaged.
495
+ *
496
+ * @remarks
497
+ * `qualify` **must** map each rejection cause into a modeled error `E` or a
498
+ * {@link Defect}. The returned `AsyncResult`'s internal promise never rejects;
499
+ * `await`-ing it always yields a `Result`. A throw inside `qualify` is itself a
500
+ * `Defect`.
501
+ *
502
+ * @typeParam T - the resolved value type.
503
+ * @typeParam E - the modeled error type.
504
+ * @param promise - the promise, or a thunk returning one.
505
+ * @param qualify - triages a rejection cause into `E` or a `Defect`.
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * import { fromPromise, defect } from "unthrown";
510
+ * const user = await fromPromise(fetchUser(id), (cause) =>
511
+ * cause instanceof NotFoundError ? ("not_found" as const) : defect(cause),
512
+ * );
513
+ * ```
514
+ */
515
+ function fromPromise(promise, qualify) {
516
+ return new AsyncRes((typeof promise === "function" ? Promise.resolve().then(promise) : promise).then((value) => okRes(value), (cause) => qualifyToResult(cause, qualify)));
517
+ }
518
+ /**
519
+ * Wrap a `Promise` asserted **not** to fail in any modeled way: any rejection
520
+ * becomes a `Defect`.
521
+ *
522
+ * @remarks
523
+ * Use this only when a rejection genuinely indicates a bug rather than an
524
+ * anticipated outcome — the error channel is `never`, so there is nothing to
525
+ * triage. (`await`-ing still yields a `Result`; it never throws.)
526
+ *
527
+ * @typeParam T - the resolved value type.
528
+ * @param promise - the promise, or a thunk returning one.
529
+ */
530
+ function fromSafePromise(promise) {
531
+ return new AsyncRes((typeof promise === "function" ? Promise.resolve().then(promise) : promise).then((value) => okRes(value), (cause) => defectRes(cause)));
532
+ }
533
+ function qualifyToResult(cause, qualify) {
534
+ try {
535
+ const q = qualify(cause);
536
+ return isDefectMarker(q) ? defectRes(q.cause) : errRes(q);
537
+ } catch (qErr) {
538
+ return defectRes(qErr);
539
+ }
540
+ }
541
+ /**
542
+ * Collect a tuple of {@link Result}s into a single `Result` of the tuple of
543
+ * success values.
544
+ *
545
+ * @remarks
546
+ * Short-circuits on the **first** `Err` (later entries are not inspected for
547
+ * their error); any `Defect` present **dominates**, winning even over an earlier
548
+ * `Err`. Positional types are preserved, so `all([ok(1), ok("a")])` is
549
+ * `Result<[number, string], …>`.
550
+ *
551
+ * @typeParam Rs - the tuple of input `Result` types.
552
+ * @param results - the results to combine.
553
+ *
554
+ * @example
555
+ * ```ts
556
+ * import { all, ok } from "unthrown";
557
+ * all([ok(1), ok("a"), ok(true)]).unwrap(); // [1, "a", true]
558
+ * ```
559
+ */
560
+ function all(results) {
561
+ const values = [];
562
+ let firstErr;
563
+ let firstDefect;
564
+ for (const r of results) if (r.tag === "Defect") firstDefect ??= r;
565
+ else if (r.tag === "Err") firstErr ??= r;
566
+ else values.push(r.value);
567
+ if (firstDefect) return firstDefect;
568
+ if (firstErr) return firstErr;
569
+ return ok(values);
570
+ }
571
+ //#endregion
572
+ //#region src/facade.ts
573
+ /**
574
+ * Companion object grouping the standalone entry points under a single,
575
+ * discoverable namespace: {@link Result.ok}, {@link Result.err},
576
+ * {@link Result.defect}, {@link Result.fromNullable}, {@link Result.fromThrowable},
577
+ * {@link Result.fromPromise}, {@link Result.fromSafePromise}, {@link Result.all},
578
+ * {@link Result.isOk}, {@link Result.isErr}, {@link Result.isDefect}.
579
+ *
580
+ * @remarks
581
+ * Purely additive sugar — each member **is** the corresponding free function.
582
+ * The free functions remain the primary, tree-shakeable API; importing only
583
+ * `{ ok }` never pulls this object in. The value `Result` and the type
584
+ * {@link Result} share one name (the companion-object pattern).
585
+ *
586
+ * @example
587
+ * ```ts
588
+ * import { Result } from "unthrown";
589
+ * Result.ok(1).flatMap((n) => Result.ok(n + 1)).unwrap(); // 2
590
+ * ```
591
+ */
592
+ const Result = {
593
+ ok,
594
+ err,
595
+ defect,
596
+ fromNullable,
597
+ fromThrowable,
598
+ fromPromise,
599
+ fromSafePromise,
600
+ all,
601
+ isOk,
602
+ isErr,
603
+ isDefect
604
+ };
605
+ //#endregion
606
+ //#region src/tagged.ts
607
+ /**
608
+ * Build a base class for a tagged error — a class extending `Error` with a
609
+ * `_tag` string discriminant, in the style of Effect's `Data.TaggedError`.
610
+ *
611
+ * @remarks
612
+ * Extend the returned class to declare a concrete error. Supply the payload with
613
+ * an instantiation expression; omit it for a payload-less error. A `message`
614
+ * field in the payload is forwarded to `Error`. The `_tag` always reflects
615
+ * `tag` and cannot be overridden by the payload.
616
+ *
617
+ * @typeParam Tag - the string literal discriminant.
618
+ * @param tag - the discriminant value, also used as the error `name`.
619
+ *
620
+ * @example
621
+ * ```ts
622
+ * class NotFound extends TaggedError("NotFound") {}
623
+ * class HttpError extends TaggedError("HttpError")<{ status: number }> {}
624
+ *
625
+ * new NotFound()._tag; // "NotFound"
626
+ * new HttpError({ status: 500 }).status; // 500
627
+ * ```
628
+ */
629
+ function TaggedError(tag) {
630
+ class TaggedErrorBase extends Error {
631
+ _tag;
632
+ constructor(props) {
633
+ super(typeof props?.["message"] === "string" ? props["message"] : void 0);
634
+ if (props) Object.assign(this, props);
635
+ this._tag = tag;
636
+ this.name = tag;
637
+ Object.setPrototypeOf(this, new.target.prototype);
638
+ }
639
+ }
640
+ return TaggedErrorBase;
641
+ }
642
+ function matchTags(result, handlers) {
643
+ const onErr = (error) => {
644
+ const handler = handlers[error._tag];
645
+ return handler(error);
646
+ };
647
+ return result.match({
648
+ ok: handlers.Ok,
649
+ err: onErr,
650
+ defect: handlers.Defect
651
+ });
652
+ }
653
+ //#endregion
654
+ exports.Result = Result;
655
+ exports.TaggedError = TaggedError;
656
+ exports.UnwrapError = UnwrapError;
657
+ exports.all = all;
658
+ exports.defect = defect;
659
+ exports.err = err;
660
+ exports.fromNullable = fromNullable;
661
+ exports.fromPromise = fromPromise;
662
+ exports.fromSafePromise = fromSafePromise;
663
+ exports.fromThrowable = fromThrowable;
664
+ exports.isDefect = isDefect;
665
+ exports.isErr = isErr;
666
+ exports.isOk = isOk;
667
+ exports.matchTags = matchTags;
668
+ exports.ok = ok;