unthrown 5.11.0 → 5.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1206,12 +1206,15 @@ function DoAsync() {
1206
1206
  *
1207
1207
  * @remarks
1208
1208
  * `null` and `undefined` map to `Err(onAbsent())`; any other value (including
1209
- * falsy ones like `0`, `""`, `false`) maps to `Ok`.
1209
+ * falsy ones like `0`, `""`, `false`) maps to `Ok`. A throw inside `onAbsent`
1210
+ * becomes a `Defect`. Its return must be synchronous ({@link NotThenable});
1211
+ * a thenable slipped past the types becomes a `Defect`, with an orphaned
1212
+ * Promise silenced and a lazy thenable left unstarted.
1210
1213
  *
1211
1214
  * @typeParam T - the (nullable) value type.
1212
1215
  * @typeParam E - the error produced when the value is absent.
1213
1216
  * @param value - the possibly-absent value.
1214
- * @param onAbsent - lazily produces the error for the absent case.
1217
+ * @param onAbsent - synchronously and lazily produces the error for the absent case.
1215
1218
  *
1216
1219
  * @category Interop
1217
1220
  *
@@ -1226,7 +1229,13 @@ function DoAsync() {
1226
1229
  * ```
1227
1230
  */
1228
1231
  function fromNullable(value, onAbsent) {
1229
- return value === null || value === void 0 ? Err(onAbsent()) : Ok(value);
1232
+ if (value !== null && value !== void 0) return Ok(value);
1233
+ try {
1234
+ const error = onAbsent();
1235
+ return isThenable(error) ? thenableReturnDefect(error, "unthrown: fromNullable's onAbsent must be synchronous — it returned a thenable") : Err(error);
1236
+ } catch (cause) {
1237
+ return defectRes(cause);
1238
+ }
1230
1239
  }
1231
1240
  /**
1232
1241
  * Wrap a throwing synchronous function so it returns a {@link Result} instead of
package/dist/index.d.cts CHANGED
@@ -63,6 +63,32 @@ type UniversalPattern = PatternMatcher<unknown> & {
63
63
  * @internal
64
64
  */
65
65
  type MatchedOf<Pt> = Pt extends PatternMatcher<infer M> ? M : Pt extends object ? { [K in keyof Pt]: MatchedOf<Pt[K]>; } : Pt;
66
+ /** Whether a value's type describes multiple alternatives, not one literal. */
67
+ type IsUnion<T, Whole = T> = T extends Whole ? ([Whole] extends [T] ? false : true) : never;
68
+ /**
69
+ * What a pattern is guaranteed to cover, rather than merely able to match.
70
+ *
71
+ * A value pattern is an equality test, so it discharges a case only when its
72
+ * type is a **unit type** — one inhabitant: a string / number / bigint /
73
+ * boolean literal, `null`, `undefined`, a `unique symbol` — or a plain object
74
+ * of such fields, recursively (so a variable inferred as `{ _tag: string }`
75
+ * cannot make an entire tagged union exhaustive). Everything else covers
76
+ * nothing: a widened `string`, a template literal (`` `E_${string}` `` is as
77
+ * wide as `string`), a union (one runtime value however many alternatives its
78
+ * type lists — a union of `P.*` patterns included, which is why the union test
79
+ * runs before the predicate one), an array or a function (compared by identity
80
+ * at runtime). Keep `MatchedOf` for handler narrowing; subtract only this
81
+ * coverage. Branded predicate patterns still cover their declared type: unlike
82
+ * a value, they test the whole type. An allow-list rather than a deny-list of
83
+ * wide types, so a shape it did not anticipate fails closed. One gap is
84
+ * accepted: a class instance is structurally an object type, so it recurses
85
+ * like a plain object while the runtime compares it by identity — pinned in
86
+ * `types.test-d.ts`.
87
+ *
88
+ * @internal
89
+ */
90
+ type CoveredBy<Pt> = true extends IsUnion<Pt> ? never : [Pt] extends [PatternMatcher<infer M>] ? M : Pt extends string ? {} extends Record<Pt, never> ? never : Pt : Pt extends number ? number extends Pt ? never : Pt : Pt extends bigint ? bigint extends Pt ? never : Pt : Pt extends symbol ? symbol extends Pt ? never : Pt : Pt extends boolean | null | undefined ? Pt : Pt extends readonly unknown[] | ((...args: never) => unknown) ? never : Pt extends object ? { [K in keyof Pt]: CoveredBy<Pt[K]>; } : never;
91
+ type CoveredByPatterns<Pts extends readonly unknown[]> = { [I in keyof Pts]: CoveredBy<Pts[I]>; }[number];
66
92
  /**
67
93
  * Reject a **keyless object pattern** (`{}`) where it is written.
68
94
  *
@@ -173,9 +199,11 @@ type Matcher<E, Remaining, O, Declared = Unset> = {
173
199
  * patterns — `matcher.with(P.tag("A"), P.tag("B"), handler)`). The handler
174
200
  * receives the input narrowed to what the patterns match (computed against
175
201
  * `Remaining`, so cases already handled by earlier arms are excluded); the
176
- * matched cases are subtracted from `Remaining`.
202
+ * guaranteed covered cases are subtracted from `Remaining`. A widened value
203
+ * pattern (e.g. `{ _tag: string }`) can match but proves no coverage: use
204
+ * literal patterns / `P.tag` to discharge the remaining cases.
177
205
  */
178
- with<const Pts extends readonly [unknown, ...unknown[]], O2>(...args: [...patterns: { [I in keyof Pts]: NoEmptyPattern<Pts[I]>; }, handler: (value: Extract<Remaining, MatchedOf<Pts[number]>>) => BranchReturn<Declared, O2>]): Matcher<E, Exclude<Remaining, MatchedOf<Pts[number]>>, O | O2, Declared>;
206
+ with<const Pts extends readonly [unknown, ...unknown[]], O2>(...args: [...patterns: { [I in keyof Pts]: NoEmptyPattern<Pts[I]>; }, handler: (value: Extract<Remaining, MatchedOf<Pts[number]>>) => BranchReturn<Declared, O2>]): Matcher<E, Exclude<Remaining, CoveredByPatterns<Pts>>, O | O2, Declared>;
179
207
  /**
180
208
  * Declare the match's output type up front: every subsequent branch handler
181
209
  * is checked against `R`, and the match evaluates to `R` instead of the
@@ -504,12 +532,15 @@ export declare function DoAsync(): AsyncResult<{}, never>;
504
532
  *
505
533
  * @remarks
506
534
  * `null` and `undefined` map to `Err(onAbsent())`; any other value (including
507
- * falsy ones like `0`, `""`, `false`) maps to `Ok`.
535
+ * falsy ones like `0`, `""`, `false`) maps to `Ok`. A throw inside `onAbsent`
536
+ * becomes a `Defect`. Its return must be synchronous ({@link NotThenable});
537
+ * a thenable slipped past the types becomes a `Defect`, with an orphaned
538
+ * Promise silenced and a lazy thenable left unstarted.
508
539
  *
509
540
  * @typeParam T - the (nullable) value type.
510
541
  * @typeParam E - the error produced when the value is absent.
511
542
  * @param value - the possibly-absent value.
512
- * @param onAbsent - lazily produces the error for the absent case.
543
+ * @param onAbsent - synchronously and lazily produces the error for the absent case.
513
544
  *
514
545
  * @category Interop
515
546
  *
@@ -523,7 +554,7 @@ export declare function DoAsync(): AsyncResult<{}, never>;
523
554
  * fromNullable(0, () => "absent").getOr(-1); // => 0 (falsy but present)
524
555
  * ```
525
556
  */
526
- export declare function fromNullable<T, E>(value: T | null | undefined, onAbsent: () => E): Result<NonNullable<T>, E>;
557
+ export declare function fromNullable<T, E>(value: T | null | undefined, onAbsent: () => E & NotThenable<E>): Result<NonNullable<T>, E>;
527
558
  /**
528
559
  * Wrap a throwing synchronous function so it returns a {@link Result} instead of
529
560
  * throwing.
package/dist/index.d.mts CHANGED
@@ -63,6 +63,32 @@ type UniversalPattern = PatternMatcher<unknown> & {
63
63
  * @internal
64
64
  */
65
65
  type MatchedOf<Pt> = Pt extends PatternMatcher<infer M> ? M : Pt extends object ? { [K in keyof Pt]: MatchedOf<Pt[K]>; } : Pt;
66
+ /** Whether a value's type describes multiple alternatives, not one literal. */
67
+ type IsUnion<T, Whole = T> = T extends Whole ? ([Whole] extends [T] ? false : true) : never;
68
+ /**
69
+ * What a pattern is guaranteed to cover, rather than merely able to match.
70
+ *
71
+ * A value pattern is an equality test, so it discharges a case only when its
72
+ * type is a **unit type** — one inhabitant: a string / number / bigint /
73
+ * boolean literal, `null`, `undefined`, a `unique symbol` — or a plain object
74
+ * of such fields, recursively (so a variable inferred as `{ _tag: string }`
75
+ * cannot make an entire tagged union exhaustive). Everything else covers
76
+ * nothing: a widened `string`, a template literal (`` `E_${string}` `` is as
77
+ * wide as `string`), a union (one runtime value however many alternatives its
78
+ * type lists — a union of `P.*` patterns included, which is why the union test
79
+ * runs before the predicate one), an array or a function (compared by identity
80
+ * at runtime). Keep `MatchedOf` for handler narrowing; subtract only this
81
+ * coverage. Branded predicate patterns still cover their declared type: unlike
82
+ * a value, they test the whole type. An allow-list rather than a deny-list of
83
+ * wide types, so a shape it did not anticipate fails closed. One gap is
84
+ * accepted: a class instance is structurally an object type, so it recurses
85
+ * like a plain object while the runtime compares it by identity — pinned in
86
+ * `types.test-d.ts`.
87
+ *
88
+ * @internal
89
+ */
90
+ type CoveredBy<Pt> = true extends IsUnion<Pt> ? never : [Pt] extends [PatternMatcher<infer M>] ? M : Pt extends string ? {} extends Record<Pt, never> ? never : Pt : Pt extends number ? number extends Pt ? never : Pt : Pt extends bigint ? bigint extends Pt ? never : Pt : Pt extends symbol ? symbol extends Pt ? never : Pt : Pt extends boolean | null | undefined ? Pt : Pt extends readonly unknown[] | ((...args: never) => unknown) ? never : Pt extends object ? { [K in keyof Pt]: CoveredBy<Pt[K]>; } : never;
91
+ type CoveredByPatterns<Pts extends readonly unknown[]> = { [I in keyof Pts]: CoveredBy<Pts[I]>; }[number];
66
92
  /**
67
93
  * Reject a **keyless object pattern** (`{}`) where it is written.
68
94
  *
@@ -173,9 +199,11 @@ type Matcher<E, Remaining, O, Declared = Unset> = {
173
199
  * patterns — `matcher.with(P.tag("A"), P.tag("B"), handler)`). The handler
174
200
  * receives the input narrowed to what the patterns match (computed against
175
201
  * `Remaining`, so cases already handled by earlier arms are excluded); the
176
- * matched cases are subtracted from `Remaining`.
202
+ * guaranteed covered cases are subtracted from `Remaining`. A widened value
203
+ * pattern (e.g. `{ _tag: string }`) can match but proves no coverage: use
204
+ * literal patterns / `P.tag` to discharge the remaining cases.
177
205
  */
178
- with<const Pts extends readonly [unknown, ...unknown[]], O2>(...args: [...patterns: { [I in keyof Pts]: NoEmptyPattern<Pts[I]>; }, handler: (value: Extract<Remaining, MatchedOf<Pts[number]>>) => BranchReturn<Declared, O2>]): Matcher<E, Exclude<Remaining, MatchedOf<Pts[number]>>, O | O2, Declared>;
206
+ with<const Pts extends readonly [unknown, ...unknown[]], O2>(...args: [...patterns: { [I in keyof Pts]: NoEmptyPattern<Pts[I]>; }, handler: (value: Extract<Remaining, MatchedOf<Pts[number]>>) => BranchReturn<Declared, O2>]): Matcher<E, Exclude<Remaining, CoveredByPatterns<Pts>>, O | O2, Declared>;
179
207
  /**
180
208
  * Declare the match's output type up front: every subsequent branch handler
181
209
  * is checked against `R`, and the match evaluates to `R` instead of the
@@ -504,12 +532,15 @@ export declare function DoAsync(): AsyncResult<{}, never>;
504
532
  *
505
533
  * @remarks
506
534
  * `null` and `undefined` map to `Err(onAbsent())`; any other value (including
507
- * falsy ones like `0`, `""`, `false`) maps to `Ok`.
535
+ * falsy ones like `0`, `""`, `false`) maps to `Ok`. A throw inside `onAbsent`
536
+ * becomes a `Defect`. Its return must be synchronous ({@link NotThenable});
537
+ * a thenable slipped past the types becomes a `Defect`, with an orphaned
538
+ * Promise silenced and a lazy thenable left unstarted.
508
539
  *
509
540
  * @typeParam T - the (nullable) value type.
510
541
  * @typeParam E - the error produced when the value is absent.
511
542
  * @param value - the possibly-absent value.
512
- * @param onAbsent - lazily produces the error for the absent case.
543
+ * @param onAbsent - synchronously and lazily produces the error for the absent case.
513
544
  *
514
545
  * @category Interop
515
546
  *
@@ -523,7 +554,7 @@ export declare function DoAsync(): AsyncResult<{}, never>;
523
554
  * fromNullable(0, () => "absent").getOr(-1); // => 0 (falsy but present)
524
555
  * ```
525
556
  */
526
- export declare function fromNullable<T, E>(value: T | null | undefined, onAbsent: () => E): Result<NonNullable<T>, E>;
557
+ export declare function fromNullable<T, E>(value: T | null | undefined, onAbsent: () => E & NotThenable<E>): Result<NonNullable<T>, E>;
527
558
  /**
528
559
  * Wrap a throwing synchronous function so it returns a {@link Result} instead of
529
560
  * throwing.
package/dist/index.mjs CHANGED
@@ -1205,12 +1205,15 @@ function DoAsync() {
1205
1205
  *
1206
1206
  * @remarks
1207
1207
  * `null` and `undefined` map to `Err(onAbsent())`; any other value (including
1208
- * falsy ones like `0`, `""`, `false`) maps to `Ok`.
1208
+ * falsy ones like `0`, `""`, `false`) maps to `Ok`. A throw inside `onAbsent`
1209
+ * becomes a `Defect`. Its return must be synchronous ({@link NotThenable});
1210
+ * a thenable slipped past the types becomes a `Defect`, with an orphaned
1211
+ * Promise silenced and a lazy thenable left unstarted.
1209
1212
  *
1210
1213
  * @typeParam T - the (nullable) value type.
1211
1214
  * @typeParam E - the error produced when the value is absent.
1212
1215
  * @param value - the possibly-absent value.
1213
- * @param onAbsent - lazily produces the error for the absent case.
1216
+ * @param onAbsent - synchronously and lazily produces the error for the absent case.
1214
1217
  *
1215
1218
  * @category Interop
1216
1219
  *
@@ -1225,7 +1228,13 @@ function DoAsync() {
1225
1228
  * ```
1226
1229
  */
1227
1230
  function fromNullable(value, onAbsent) {
1228
- return value === null || value === void 0 ? Err(onAbsent()) : Ok(value);
1231
+ if (value !== null && value !== void 0) return Ok(value);
1232
+ try {
1233
+ const error = onAbsent();
1234
+ return isThenable(error) ? thenableReturnDefect(error, "unthrown: fromNullable's onAbsent must be synchronous — it returned a thenable") : Err(error);
1235
+ } catch (cause) {
1236
+ return defectRes(cause);
1237
+ }
1229
1238
  }
1230
1239
  /**
1231
1240
  * Wrap a throwing synchronous function so it returns a {@link Result} instead of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unthrown",
3
- "version": "5.11.0",
3
+ "version": "5.12.1",
4
4
  "description": "Explicit errors as values, with a separate defect (panic) channel",
5
5
  "keywords": [
6
6
  "defect",
@@ -47,11 +47,11 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@btravstack/tsconfig": "0.4.0",
50
- "@types/node": "26.6.2",
51
- "@vitest/coverage-v8": "5.0.1",
50
+ "@types/node": "26.6.3",
51
+ "@vitest/coverage-v8": "5.0.2",
52
52
  "tsdown": "0.23.0",
53
53
  "typescript": "7.0.2",
54
- "vitest": "5.0.1"
54
+ "vitest": "5.0.2"
55
55
  },
56
56
  "engines": {
57
57
  "node": ">=20"