paramour 0.9.0 → 0.10.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/codec.d.ts CHANGED
@@ -30,7 +30,7 @@ export type Arity = "many" | "single";
30
30
  * `Out` (which is an array type).
31
31
  */
32
32
  export interface Codec<Out, P extends Presence = "required", C extends boolean = false, A extends Arity = "single", E extends boolean = boolean> {
33
- readonly catch: C extends false ? (fallback: (() => Out) | Out) => Codec<Out, P, true, A, E> : never;
33
+ readonly catch: C extends false ? (fallback: (() => CatchFallback<Out, P>) | CatchFallback<Out, P>) => Codec<Out, P, true, A, E> : never;
34
34
  /**
35
35
  * Overloaded so the value/factory split is visible in type-state: the
36
36
  * factory overload comes FIRST and must stay first. The runtime
@@ -48,8 +48,13 @@ export interface Codec<Out, P extends Presence = "required", C extends boolean =
48
48
  } : never : never;
49
49
  readonly optional: A extends "single" ? P extends "required" ? () => Codec<Out, "optional", C, A, E> : never : never;
50
50
  readonly "~arity": A;
51
- /** Stored as a thunk regardless of the form passed to `.catch()`. */
52
- readonly "~catchValue": (() => Out) | undefined;
51
+ /**
52
+ * Stored as a thunk regardless of the form passed to `.catch()`. The thunk
53
+ * may return `undefined` only on an `.optional()` codec (see
54
+ * {@link CatchFallback}); typed uniformly so every codec stays assignable
55
+ * to {@link AnyCodec}.
56
+ */
57
+ readonly "~catchValue": (() => Out | undefined) | undefined;
53
58
  readonly "~caught": C;
54
59
  /**
55
60
  * True when `.default()` received a value (not a factory). Value defaults
@@ -104,6 +109,17 @@ export type ParamCodec = Codec<any, "required", boolean>;
104
109
  */
105
110
  export type Presence = "defaulted" | "optional" | "required";
106
111
  export type PresenceOf<C extends AnyCodec> = C["~presence"];
112
+ /**
113
+ * What `.catch()` may recover to. An `.optional()` codec already decodes to
114
+ * `Out | undefined`, so it may also recover a failed parse to *absent* —
115
+ * `undefined` — which is the honest fallback when no in-domain value means
116
+ * "nothing selected". This does not blur D2: the INPUT `.catch()` handles is
117
+ * still a present-but-malformed value, never absence. Every other presence
118
+ * keeps an `Out`-only fallback: a required or defaulted key must decode to a
119
+ * value. Non-distributive so the `Presence` union inside {@link AnyCodec}
120
+ * reads as `Out`, keeping concrete optional codecs assignable to it.
121
+ */
122
+ type CatchFallback<Out, P extends Presence> = [P] extends ["optional"] ? Out | undefined : Out;
107
123
  /**
108
124
  * Rejects value-form `.default()` arguments whose static type includes any
109
125
  * function member: runtime {@link isFactory} would treat them as factories,
package/dist/codec.js CHANGED
@@ -22,7 +22,28 @@ function build(state) {
22
22
  if (state.catchValue !== undefined) {
23
23
  throw new ParamourError(".catch() may only be applied once");
24
24
  }
25
- return build({ ...state, catchValue: toThunk(fallback, "catch") });
25
+ if (state.presence === "optional") {
26
+ return build({ ...state, catchValue: toThunk(fallback, "catch") });
27
+ }
28
+ // Only an optional key may recover to absent (CatchFallback). The
29
+ // value form fails here, at definition time; a factory's result is
30
+ // only knowable per decode, so it is checked there.
31
+ if (fallback === undefined) {
32
+ throw new ParamourError(".catch(undefined) requires .optional() first: only an optional key can recover to absent");
33
+ }
34
+ const thunk = toThunk(fallback, "catch");
35
+ return build({
36
+ ...state,
37
+ catchValue: isFactory(fallback)
38
+ ? () => {
39
+ const value = thunk();
40
+ if (value === undefined) {
41
+ throw new ParamourError(".catch() factory returned undefined on a non-optional codec: only an optional key can recover to absent");
42
+ }
43
+ return value;
44
+ }
45
+ : thunk,
46
+ });
26
47
  },
27
48
  default(value) {
28
49
  if (state.arity === "many") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paramour",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {