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