@onrails/pattern 0.1.1 → 0.1.3
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 +37 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -12
- package/dist/index.d.ts +29 -12
- package/dist/index.js +37 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ var matches = (input, pattern) => {
|
|
|
28
28
|
return true;
|
|
29
29
|
};
|
|
30
30
|
var NO_MATCH = /* @__PURE__ */ Symbol("@onrails/pattern/no-match");
|
|
31
|
+
var NO_INPUT = /* @__PURE__ */ Symbol("@onrails/pattern/no-input");
|
|
31
32
|
var runCases = (input, cases) => {
|
|
32
33
|
for (const c of cases) {
|
|
33
34
|
if (c.test(input)) {
|
|
@@ -41,7 +42,7 @@ var caseForPatterns = (patterns, handler) => ({
|
|
|
41
42
|
run: handler
|
|
42
43
|
});
|
|
43
44
|
var MatchBuilder = class _MatchBuilder {
|
|
44
|
-
constructor(cases
|
|
45
|
+
constructor(cases, input, _handled = []) {
|
|
45
46
|
this.cases = cases;
|
|
46
47
|
this.input = input;
|
|
47
48
|
this._handled = _handled;
|
|
@@ -49,21 +50,26 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
49
50
|
cases;
|
|
50
51
|
input;
|
|
51
52
|
_handled;
|
|
53
|
+
/** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
|
|
54
|
+
addCase(next) {
|
|
55
|
+
return new _MatchBuilder(
|
|
56
|
+
[...this.cases, next],
|
|
57
|
+
this.input,
|
|
58
|
+
// Safe: _handled is a phantom tuple — only its type-level contents matter.
|
|
59
|
+
this._handled
|
|
60
|
+
);
|
|
61
|
+
}
|
|
52
62
|
with(pattern, handler) {
|
|
53
|
-
|
|
63
|
+
return this.addCase({
|
|
54
64
|
test: (input) => matches(input, pattern),
|
|
55
65
|
run: handler
|
|
56
|
-
};
|
|
57
|
-
return new _MatchBuilder([...this.cases, next], this.input, [
|
|
58
|
-
...this._handled
|
|
59
|
-
]);
|
|
66
|
+
});
|
|
60
67
|
}
|
|
61
68
|
/** One handler for several patterns (OR). Handler input is the union of narrowed members. */
|
|
62
69
|
withOneOf(patterns, handler) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
]);
|
|
70
|
+
return this.addCase(
|
|
71
|
+
caseForPatterns(patterns, handler)
|
|
72
|
+
);
|
|
67
73
|
}
|
|
68
74
|
/** `withOneOf` for exactly two patterns. */
|
|
69
75
|
withEither(pattern1, pattern2, handler) {
|
|
@@ -73,14 +79,28 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
73
79
|
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
74
80
|
* Useful when branch return-type inference widens to a union narrower than
|
|
75
81
|
* the slot the match feeds into (e.g. `ReactNode`).
|
|
82
|
+
*
|
|
83
|
+
* Also the fix for literal-widening: when a handler returns an object whose
|
|
84
|
+
* field is a union literal, inference widens it (e.g. `{ mode: "native" }`
|
|
85
|
+
* becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type
|
|
86
|
+
* instead of sprinkling `as const`:
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* type State = { mode: "compat" | "native" };
|
|
91
|
+
* const next = match(flag)
|
|
92
|
+
* .returnType<State>() // handlers now checked against State
|
|
93
|
+
* .with("--native", () => ({ mode: "native" })) // no `as const` needed
|
|
94
|
+
* .otherwise(() => state);
|
|
95
|
+
* ```
|
|
76
96
|
*/
|
|
77
97
|
returnType() {
|
|
78
98
|
return new _MatchBuilder(this.cases, this.input, this._handled);
|
|
79
99
|
}
|
|
80
100
|
/** Run on the value passed to {@link match}, or on `input` when curried. */
|
|
81
|
-
run(input) {
|
|
82
|
-
const value = input
|
|
83
|
-
if (value ===
|
|
101
|
+
run(...input) {
|
|
102
|
+
const value = input.length === 1 ? input[0] : this.input;
|
|
103
|
+
if (value === NO_INPUT) {
|
|
84
104
|
throw new Error("match.run: no input value");
|
|
85
105
|
}
|
|
86
106
|
const out = runCases(value, this.cases);
|
|
@@ -90,24 +110,18 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
90
110
|
return out;
|
|
91
111
|
}
|
|
92
112
|
exhaustive() {
|
|
93
|
-
|
|
94
|
-
return this.run();
|
|
95
|
-
}
|
|
96
|
-
return ((value) => this.run(value));
|
|
113
|
+
return this.input === NO_INPUT ? (value) => this.run(value) : this.run();
|
|
97
114
|
}
|
|
98
115
|
otherwise(handler) {
|
|
99
116
|
const runWithFallback = (value) => {
|
|
100
117
|
const out = runCases(value, this.cases);
|
|
101
118
|
return out === NO_MATCH ? handler(value) : out;
|
|
102
119
|
};
|
|
103
|
-
|
|
104
|
-
return runWithFallback(this.input);
|
|
105
|
-
}
|
|
106
|
-
return runWithFallback;
|
|
120
|
+
return this.input === NO_INPUT ? runWithFallback : runWithFallback(this.input);
|
|
107
121
|
}
|
|
108
122
|
};
|
|
109
|
-
function match(input) {
|
|
110
|
-
return new MatchBuilder([], input);
|
|
123
|
+
function match(...input) {
|
|
124
|
+
return input.length === 1 ? new MatchBuilder([], input[0]) : new MatchBuilder([], NO_INPUT);
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
// src/tag.ts
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/assert.ts","../src/match.ts","../src/tag.ts","../src/when.ts"],"names":[],"mappings":";;;AACO,IAAM,WAAA,GAAc,CAAC,KAAA,EAAc,OAAA,GAAU,aAAA,KAAyB;AAC3E,EAAA,MAAM,IAAI,MAAM,CAAA,EAAG,OAAO,KAAK,MAAA,CAAO,KAAK,CAAC,CAAA,CAAE,CAAA;AAChD;;;ACgCA,IAAM,OAAA,GAAU,CAAI,OAAA,KAClB,OAAO,OAAA,KAAY,UAAA;AAErB,IAAM,kBAAkB,CAAC,OAAA,KACvB,OAAO,OAAA,KAAY,YAAY,OAAA,KAAY,IAAA;AAE7C,IAAM,OAAA,GAAU,CAAI,KAAA,EAAU,OAAA,KAAiC;AAC7D,EAAA,IAAI,OAAA,CAAQ,OAAO,CAAA,EAAG;AACpB,IAAA,OAAO,QAAQ,KAAK,CAAA;AAAA,EACtB;AACA,EAAA,IAAI,CAAC,eAAA,CAAgB,OAAO,CAAA,EAAG;AAC7B,IAAA,OAAO,KAAA,KAAU,OAAA;AAAA,EACnB;AACA,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,IAAA,EAAM;AAC/C,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,MAAM,MAAA,GAAS,KAAA;AACf,EAAA,MAAM,aAAA,GAAgB,OAAA;AACtB,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,aAAa,CAAA,EAAG;AAC5C,IAAA,IAAI,MAAA,CAAO,GAAG,CAAA,KAAM,aAAA,CAAc,GAAG,CAAA,EAAG;AACtC,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT,CAAA;AAKA,IAAM,QAAA,0BAAkB,2BAA2B,CAAA;AAGnD,IAAM,QAAA,GAAW,CAAO,KAAA,EAAU,KAAA,KAA8C;AAC9E,EAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,IAAA,IAAI,CAAA,CAAE,IAAA,CAAK,KAAK,CAAA,EAAG;AACjB,MAAA,OAAO,CAAA,CAAE,IAAI,KAAK,CAAA;AAAA,IACpB;AAAA,EACF;AACA,EAAA,OAAO,QAAA;AACT,CAAA;AAEA,IAAM,eAAA,GAAkB,CACtB,QAAA,EACA,OAAA,MACgB;AAAA,EAChB,IAAA,EAAM,CAAC,KAAA,KAAU,QAAA,CAAS,IAAA,CAAK,CAAC,OAAA,KAAY,OAAA,CAAQ,KAAA,EAAO,OAAO,CAAC,CAAA;AAAA,EACnE,GAAA,EAAK;AACP,CAAA,CAAA;AA2BO,IAAM,YAAA,GAAN,MAAM,aAAA,CAMX;AAAA,EACA,YACmB,KAAA,GAAqC,IACrC,KAAA,EAER,QAAA,GAAoB,EAAC,EAC9B;AAJiB,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AACA,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAER,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA,EACR;AAAA,EAJgB,KAAA;AAAA,EACA,KAAA;AAAA,EAER,QAAA;AAAA,EAGX,IAAA,CACE,SACA,OAAA,EAOA;AACA,IAAA,MAAM,IAAA,GAAyB;AAAA,MAC7B,IAAA,EAAM,CAAC,KAAA,KAAU,OAAA,CAAQ,OAAO,OAAO,CAAA;AAAA,MACvC,GAAA,EAAK;AAAA,KACP;AACA,IAAA,OAAO,IAAI,cAAa,CAAC,GAAG,KAAK,KAAA,EAAO,IAAI,CAAA,EAAG,IAAA,CAAK,KAAA,EAAO;AAAA,MACzD,GAAG,IAAA,CAAK;AAAA,KACiD,CAAA;AAAA,EAC7D;AAAA;AAAA,EAGA,SAAA,CACE,UACA,OAAA,EAOA;AACA,IAAA,MAAM,IAAA,GAAO,eAAA,CAAgB,QAAA,EAAU,OAAgC,CAAA;AACvE,IAAA,OAAO,IAAI,cAAa,CAAC,GAAG,KAAK,KAAA,EAAO,IAAI,CAAA,EAAG,IAAA,CAAK,KAAA,EAAO;AAAA,MACzD,GAAG,IAAA,CAAK;AAAA,KAC+C,CAAA;AAAA,EAC3D;AAAA;AAAA,EAGA,UAAA,CACE,QAAA,EACA,QAAA,EACA,OAAA,EAOA;AACA,IAAA,OAAO,KAAK,SAAA,CAAU,CAAC,QAAA,EAAU,QAAQ,GAAG,OAAO,CAAA;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UAAA,GAA+D;AAC7D,IAAA,OAAO,IAAI,aAAA,CAA6C,IAAA,CAAK,OAAO,IAAA,CAAK,KAAA,EAAO,KAAK,QAAQ,CAAA;AAAA,EAC/F;AAAA;AAAA,EAGA,IAAI,KAAA,EAAc;AAChB,IAAA,MAAM,KAAA,GAAQ,SAAS,IAAA,CAAK,KAAA;AAC5B,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,MAAM,IAAI,MAAM,2BAA2B,CAAA;AAAA,IAC7C;AACA,IAAA,MAAM,GAAA,GAAM,QAAA,CAAS,KAAA,EAAO,IAAA,CAAK,KAAK,CAAA;AACtC,IAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,MAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,IAC/D;AACA,IAAA,OAAO,GAAA;AAAA,EACT;AAAA,EAEA,UAAA,GAAwD;AACtD,IAAA,IAAI,IAAA,CAAK,UAAU,MAAA,EAAW;AAC5B,MAAA,OAAO,KAAK,GAAA,EAAI;AAAA,IAClB;AACA,IAAA,QAAQ,CAAC,KAAA,KAAa,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA;AAAA,EACtC;AAAA,EAEA,UAAU,OAAA,EAAuE;AAC/E,IAAA,MAAM,eAAA,GAAkB,CAAC,KAAA,KAAgB;AACvC,MAAA,MAAM,GAAA,GAAM,QAAA,CAAS,KAAA,EAAO,IAAA,CAAK,KAAK,CAAA;AACtC,MAAA,OAAO,GAAA,KAAQ,QAAA,GAAW,OAAA,CAAQ,KAAK,CAAA,GAAK,GAAA;AAAA,IAC9C,CAAA;AACA,IAAA,IAAI,IAAA,CAAK,UAAU,MAAA,EAAW;AAC5B,MAAA,OAAO,eAAA,CAAgB,KAAK,KAAK,CAAA;AAAA,IACnC;AACA,IAAA,OAAO,eAAA;AAAA,EACT;AACF;AAuCO,SAAS,MAAS,KAAA,EAA4C;AACnE,EAAA,OAAO,IAAI,YAAA,CAAa,EAAC,EAAG,KAAK,CAAA;AACnC;;;ACrPO,IAAM,QAAA,GAAW,CAAsB,KAAA,EAAU,QAAA,KAAmC;AACzF,EAAA,MAAM,MAAM,KAAA,CAAM,IAAA;AAClB,EAAA,MAAM,OAAA,GAAU,SAAS,GAAG,CAAA;AAC5B,EAAA,OAAO,QAAQ,KAAyC,CAAA;AAC1D;;;ACFO,SAAS,KAAQ,KAAA,EAA0C;AAChE,EAAA,OAAO,KAAA;AACT","file":"index.cjs","sourcesContent":["/** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */\nexport const assertNever = (value: never, message = \"Unreachable\"): never => {\n throw new Error(`${message}: ${String(value)}`);\n};\n","import type { ExhaustiveResult, ExhaustMatched } from \"./exhaustive.js\";\nimport type { Narrow, NarrowUnion } from \"./narrow.js\";\n\n/**\n * A pattern that matches a value of type `T`. One of:\n *\n * - **Literal** — a primitive (`\"a\"`, `42`, `true`) matched by `===`.\n * - **Object pattern** — a `Partial<T>` of shallow key/value pairs that\n * must all match by strict equality. Only available when `T` is an\n * object type.\n * - **Guard** — a function `(input: T) => boolean`. Type predicates\n * (`(x): x is U`) narrow the handler input via {@link Narrow}.\n *\n * @example\n * ```ts\n * type Event = { kind: \"click\"; x: number } | { kind: \"key\"; code: string };\n *\n * const p1: Pattern<Event> = { kind: \"click\" }; // object pattern\n * const p2: Pattern<Event> = (e) => e.kind === \"key\"; // guard\n * ```\n */\nexport type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;\n\n// Shallow partial of `T`; restricted to object types so primitive `T`\n// (e.g. `number | string`) does not pick up `Record<string, unknown>`\n// as a candidate pattern and pollute `Narrow<T, P>` via distribution.\ntype ObjectPattern<T> = T extends object ? Partial<T> : never;\n\ntype PatternGuard<T> = (input: T) => boolean;\n\ntype Case<T, R> = {\n readonly test: (input: T) => boolean;\n readonly run: (input: T) => R;\n};\n\nconst isGuard = <T>(pattern: Pattern<T>): pattern is PatternGuard<T> =>\n typeof pattern === \"function\";\n\nconst isPatternObject = (pattern: unknown): pattern is Record<string, unknown> =>\n typeof pattern === \"object\" && pattern !== null;\n\nconst matches = <T>(input: T, pattern: Pattern<T>): boolean => {\n if (isGuard(pattern)) {\n return pattern(input);\n }\n if (!isPatternObject(pattern)) {\n return input === pattern;\n }\n if (typeof input !== \"object\" || input === null) {\n return false;\n }\n const record = input as Record<string, unknown>;\n const patternRecord = pattern as Record<string, unknown>;\n for (const key of Object.keys(patternRecord)) {\n if (record[key] !== patternRecord[key]) {\n return false;\n }\n }\n return true;\n};\n\n// Sentinel for \"no case matched\". Distinct from a handler legitimately\n// returning `undefined` (e.g. side-effect-only handlers), which would\n// otherwise be misreported as non-exhaustive.\nconst NO_MATCH = Symbol(\"@onrails/pattern/no-match\");\ntype NoMatch = typeof NO_MATCH;\n\nconst runCases = <T, R>(input: T, cases: readonly Case<T, R>[]): R | NoMatch => {\n for (const c of cases) {\n if (c.test(input)) {\n return c.run(input);\n }\n }\n return NO_MATCH;\n};\n\nconst caseForPatterns = <T, R>(\n patterns: readonly Pattern<T>[],\n handler: (input: T) => R,\n): Case<T, R> => ({\n test: (input) => patterns.some((pattern) => matches(input, pattern)),\n run: handler,\n});\n\n// Result-type accumulator: stays at `R` when the result type is locked\n// (after `returnType<R>()`), otherwise widens with each `.with`.\ntype NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;\n\n// Handler return-type constraint: when locked, every handler must return `R`;\n// when open, the handler can return any type and the result widens to include it.\ntype HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;\n\n/**\n * Fluent builder for {@link match}. Tracks the handled cases at the type\n * level so `.exhaustive()` only compiles when every union member is covered.\n *\n * The `Locked` phantom flag (set via `.returnType<R>()`) constrains every\n * subsequent handler to return `R`, useful when branch return-type\n * inference would widen to a union narrower than the target slot\n * (e.g. `ReactNode`, an API DTO).\n *\n * @example\n * ```ts\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport class MatchBuilder<\n T,\n R = never,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n Locked extends boolean = false,\n> {\n constructor(\n private readonly cases: readonly Case<T, unknown>[] = [],\n private readonly input?: T,\n // Safe: phantom tuple — only used for compile-time exhaustiveness tracking.\n readonly _handled: Handled = [] as unknown as Handled,\n ) {}\n\n with<const P extends Pattern<T>, R2>(\n pattern: P,\n handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, ExhaustMatched<T, P>],\n Locked\n > {\n const next: Case<T, unknown> = {\n test: (input) => matches(input, pattern),\n run: handler as (input: T) => unknown,\n };\n return new MatchBuilder([...this.cases, next], this.input, [\n ...this._handled,\n ] as unknown as readonly [...Handled, ExhaustMatched<T, P>]);\n }\n\n /** One handler for several patterns (OR). Handler input is the union of narrowed members. */\n withOneOf<const Ps extends readonly Pattern<T>[], R2>(\n patterns: Ps,\n handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, Ps>],\n Locked\n > {\n const next = caseForPatterns(patterns, handler as (input: T) => unknown);\n return new MatchBuilder([...this.cases, next], this.input, [\n ...this._handled,\n ] as unknown as readonly [...Handled, NarrowUnion<T, Ps>]);\n }\n\n /** `withOneOf` for exactly two patterns. */\n withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(\n pattern1: P1,\n pattern2: P2,\n handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>],\n Locked\n > {\n return this.withOneOf([pattern1, pattern2], handler);\n }\n\n /**\n * Lock the result type. All subsequent `.with()` handlers must return `R2`.\n * Useful when branch return-type inference widens to a union narrower than\n * the slot the match feeds into (e.g. `ReactNode`).\n */\n returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true> {\n return new MatchBuilder<T, R2, HasInput, Handled, true>(this.cases, this.input, this._handled);\n }\n\n /** Run on the value passed to {@link match}, or on `input` when curried. */\n run(input?: T): R {\n const value = input ?? this.input;\n if (value === undefined) {\n throw new Error(\"match.run: no input value\");\n }\n const out = runCases(value, this.cases);\n if (out === NO_MATCH) {\n throw new Error(\"Non-exhaustive match: no case matched input\");\n }\n return out as R;\n }\n\n exhaustive(): ExhaustiveResult<T, Handled, R, HasInput> {\n if (this.input !== undefined) {\n return this.run() as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n return ((value: T) => this.run(value)) as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n\n otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R {\n const runWithFallback = (value: T): R => {\n const out = runCases(value, this.cases);\n return out === NO_MATCH ? handler(value) : (out as R);\n };\n if (this.input !== undefined) {\n return runWithFallback(this.input) as HasInput extends true ? R : (input: T) => R;\n }\n return runWithFallback as HasInput extends true ? R : (input: T) => R;\n }\n}\n\n/**\n * Result-locked variant of {@link MatchBuilder}. Constructed via\n * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`\n * with the `Locked` phantom flag set — kept for backwards compatibility.\n */\nexport type LockedMatchBuilder<\n T,\n R,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n> = MatchBuilder<T, R, HasInput, Handled, true>;\n\n/**\n * Start a pattern-matching expression. Two call shapes:\n *\n * - `match(value)` — data-first; subsequent `.exhaustive()` or\n * `.otherwise(fn)` runs immediately against `value`.\n * - `match<T>()` — curried; subsequent `.exhaustive()` returns a function\n * `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.\n *\n * @example\n * ```ts\n * // Data-first\n * const out = match({ kind: \"click\", x: 1, y: 2 } as Event)\n * .with({ kind: \"click\" }, (e) => e.x + e.y)\n * .with({ kind: \"key\" }, () => -1)\n * .exhaustive();\n *\n * // Curried — build a reusable matcher\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport function match<T>(input: T): MatchBuilder<T, never, true>;\nexport function match<T = never>(): MatchBuilder<T, never, false>;\nexport function match<T>(input?: T): MatchBuilder<T, never, boolean> {\n return new MatchBuilder([], input);\n}\n","type Tagged = { readonly _tag: string };\n\ntype TagHandlers<T extends Tagged, R> = {\n [K in T[\"_tag\"]]: (value: Extract<T, { _tag: K }>) => R;\n};\n\n/**\n * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style unions.\n */\nexport const matchTag = <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>): R => {\n const tag = value._tag as T[\"_tag\"];\n const handler = handlers[tag] as (value: Extract<T, { _tag: typeof tag }>) => R;\n return handler(value as Extract<T, { _tag: typeof tag }>);\n};\n","import type { Pattern } from \"./match.js\";\n\n/**\n * Guard pattern for {@link match}.with.\n *\n * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate\n * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's\n * input is narrowed to the predicate's target type via {@link Narrow}.\n */\nexport function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T>;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T> {\n return guard;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/assert.ts","../src/match.ts","../src/tag.ts","../src/when.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */\nexport const assertNever = (value: never, message = \"Unreachable\"): never => {\n throw new Error(`${message}: ${String(value)}`);\n};\n","import type { ExhaustiveResult, ExhaustMatched } from \"./exhaustive.js\";\nimport type { Narrow, NarrowUnion } from \"./narrow.js\";\n\n/**\n * A pattern that matches a value of type `T`. One of:\n *\n * - **Literal** — a primitive (`\"a\"`, `42`, `true`) matched by `===`.\n * - **Object pattern** — a `Partial<T>` of shallow key/value pairs that\n * must all match by strict equality. Only available when `T` is an\n * object type.\n * - **Guard** — a function `(input: T) => boolean`. Type predicates\n * (`(x): x is U`) narrow the handler input via {@link Narrow}.\n *\n * @example\n * ```ts\n * type Event = { kind: \"click\"; x: number } | { kind: \"key\"; code: string };\n *\n * const p1: Pattern<Event> = { kind: \"click\" }; // object pattern\n * const p2: Pattern<Event> = (e) => e.kind === \"key\"; // guard\n * ```\n */\nexport type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;\n\n// Shallow partial of `T`; restricted to object types so primitive `T`\n// (e.g. `number | string`) does not pick up `Record<string, unknown>`\n// as a candidate pattern and pollute `Narrow<T, P>` via distribution.\ntype ObjectPattern<T> = T extends object ? Partial<T> : never;\n\ntype PatternGuard<T> = (input: T) => boolean;\n\ntype Case<T, R> = {\n readonly test: (input: T) => boolean;\n readonly run: (input: T) => R;\n};\n\nconst isGuard = <T>(pattern: Pattern<T>): pattern is PatternGuard<T> =>\n typeof pattern === \"function\";\n\nconst isPatternObject = (pattern: unknown): pattern is Record<string, unknown> =>\n typeof pattern === \"object\" && pattern !== null;\n\nconst matches = <T>(input: T, pattern: Pattern<T>): boolean => {\n if (isGuard(pattern)) {\n return pattern(input);\n }\n if (!isPatternObject(pattern)) {\n return input === pattern;\n }\n if (typeof input !== \"object\" || input === null) {\n return false;\n }\n const record = input as Record<string, unknown>;\n const patternRecord = pattern as Record<string, unknown>;\n for (const key of Object.keys(patternRecord)) {\n if (record[key] !== patternRecord[key]) {\n return false;\n }\n }\n return true;\n};\n\n// Sentinel for \"no case matched\". Distinct from a handler legitimately\n// returning `undefined` (e.g. side-effect-only handlers), which would\n// otherwise be misreported as non-exhaustive.\nconst NO_MATCH = Symbol(\"@onrails/pattern/no-match\");\ntype NoMatch = typeof NO_MATCH;\n\n// Sentinel for \"curried matcher, no captured input\". Distinct from a\n// data-first `match(undefined)` / `match(null)`, which must run immediately\n// like any other value instead of silently degrading to curried mode.\nconst NO_INPUT = Symbol(\"@onrails/pattern/no-input\");\ntype NoInput = typeof NO_INPUT;\n\nconst runCases = <T, R>(input: T, cases: readonly Case<T, R>[]): R | NoMatch => {\n for (const c of cases) {\n if (c.test(input)) {\n return c.run(input);\n }\n }\n return NO_MATCH;\n};\n\nconst caseForPatterns = <T, R>(\n patterns: readonly Pattern<T>[],\n handler: (input: T) => R,\n): Case<T, R> => ({\n test: (input) => patterns.some((pattern) => matches(input, pattern)),\n run: handler,\n});\n\n// Result-type accumulator: stays at `R` when the result type is locked\n// (after `returnType<R>()`), otherwise widens with each `.with`.\ntype NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;\n\n// Handler return-type constraint: when locked, every handler must return `R`;\n// when open, the handler can return any type and the result widens to include it.\ntype HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;\n\n/**\n * Fluent builder for {@link match}. Tracks the handled cases at the type\n * level so `.exhaustive()` only compiles when every union member is covered.\n *\n * The `Locked` phantom flag (set via `.returnType<R>()`) constrains every\n * subsequent handler to return `R`, useful when branch return-type\n * inference would widen to a union narrower than the target slot\n * (e.g. `ReactNode`, an API DTO).\n *\n * @example\n * ```ts\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport class MatchBuilder<\n T,\n R = never,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n Locked extends boolean = false,\n> {\n constructor(\n private readonly cases: readonly Case<T, unknown>[],\n // No default: `= NO_INPUT` would swallow a data-first `undefined` input,\n // because JS applies parameter defaults to explicitly-passed undefined.\n private readonly input: T | NoInput,\n // Safe: phantom tuple — only used for compile-time exhaustiveness tracking.\n readonly _handled: Handled = [] as unknown as Handled,\n ) {}\n\n /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */\n private addCase<R2, H>(\n next: Case<T, unknown>,\n ): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, H], Locked> {\n return new MatchBuilder(\n [...this.cases, next],\n this.input,\n // Safe: _handled is a phantom tuple — only its type-level contents matter.\n this._handled as unknown as readonly [...Handled, H],\n );\n }\n\n with<const P extends Pattern<T>, R2>(\n pattern: P,\n handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, ExhaustMatched<T, P>],\n Locked\n > {\n return this.addCase<R2, ExhaustMatched<T, P>>({\n test: (input) => matches(input, pattern),\n run: handler as (input: T) => unknown,\n });\n }\n\n /** One handler for several patterns (OR). Handler input is the union of narrowed members. */\n withOneOf<const Ps extends readonly Pattern<T>[], R2>(\n patterns: Ps,\n handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, Ps>],\n Locked\n > {\n return this.addCase<R2, NarrowUnion<T, Ps>>(\n caseForPatterns(patterns, handler as (input: T) => unknown),\n );\n }\n\n /** `withOneOf` for exactly two patterns. */\n withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(\n pattern1: P1,\n pattern2: P2,\n handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>],\n Locked\n > {\n return this.withOneOf([pattern1, pattern2], handler);\n }\n\n /**\n * Lock the result type. All subsequent `.with()` handlers must return `R2`.\n * Useful when branch return-type inference widens to a union narrower than\n * the slot the match feeds into (e.g. `ReactNode`).\n *\n * Also the fix for literal-widening: when a handler returns an object whose\n * field is a union literal, inference widens it (e.g. `{ mode: \"native\" }`\n * becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type\n * instead of sprinkling `as const`:\n *\n * @example\n * ```ts\n * type State = { mode: \"compat\" | \"native\" };\n * const next = match(flag)\n * .returnType<State>() // handlers now checked against State\n * .with(\"--native\", () => ({ mode: \"native\" })) // no `as const` needed\n * .otherwise(() => state);\n * ```\n */\n returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true> {\n return new MatchBuilder<T, R2, HasInput, Handled, true>(this.cases, this.input, this._handled);\n }\n\n /** Run on the value passed to {@link match}, or on `input` when curried. */\n run(...input: readonly [input: T] | readonly []): R {\n const value = input.length === 1 ? input[0] : this.input;\n if (value === NO_INPUT) {\n throw new Error(\"match.run: no input value\");\n }\n const out = runCases(value, this.cases);\n if (out === NO_MATCH) {\n throw new Error(\"Non-exhaustive match: no case matched input\");\n }\n return out as R;\n }\n\n exhaustive(): ExhaustiveResult<T, Handled, R, HasInput> {\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? (value: T) => this.run(value) : this.run()\n ) as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n\n otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R {\n const runWithFallback = (value: T): R => {\n const out = runCases(value, this.cases);\n return out === NO_MATCH ? handler(value) : (out as R);\n };\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? runWithFallback : runWithFallback(this.input)\n ) as HasInput extends true ? R : (input: T) => R;\n }\n}\n\n/**\n * Result-locked variant of {@link MatchBuilder}. Constructed via\n * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`\n * with the `Locked` phantom flag set — kept for backwards compatibility.\n */\nexport type LockedMatchBuilder<\n T,\n R,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n> = MatchBuilder<T, R, HasInput, Handled, true>;\n\n/**\n * Start a pattern-matching expression. Two call shapes:\n *\n * - `match(value)` — data-first; subsequent `.exhaustive()` or\n * `.otherwise(fn)` runs immediately against `value`.\n * - `match<T>()` — curried; subsequent `.exhaustive()` returns a function\n * `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.\n *\n * @example\n * ```ts\n * // Data-first\n * const out = match({ kind: \"click\", x: 1, y: 2 } as Event)\n * .with({ kind: \"click\" }, (e) => e.x + e.y)\n * .with({ kind: \"key\" }, () => -1)\n * .exhaustive();\n *\n * // Curried — build a reusable matcher\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport function match<T>(input: T): MatchBuilder<T, never, true>;\nexport function match<T = never>(): MatchBuilder<T, never, false>;\nexport function match<T>(\n ...input: readonly [input: T] | readonly []\n): MatchBuilder<T, never, boolean> {\n return input.length === 1\n ? new MatchBuilder<T, never, boolean>([], input[0])\n : new MatchBuilder<T, never, boolean>([], NO_INPUT);\n}\n","type Tagged = { readonly _tag: string };\n\ntype TagHandlers<T extends Tagged, R> = {\n [K in T[\"_tag\"]]: (value: Extract<T, { _tag: K }>) => R;\n};\n\n/**\n * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style unions.\n */\nexport const matchTag = <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>): R => {\n const tag = value._tag as T[\"_tag\"];\n const handler = handlers[tag] as (value: Extract<T, { _tag: typeof tag }>) => R;\n return handler(value as Extract<T, { _tag: typeof tag }>);\n};\n","import type { Pattern } from \"./match.js\";\n\n/**\n * Guard pattern for {@link match}.with.\n *\n * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate\n * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's\n * input is narrowed to the predicate's target type via {@link Narrow}.\n */\nexport function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T>;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T> {\n return guard;\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -3,12 +3,18 @@ export { matchTag } from './tag.cjs';
|
|
|
3
3
|
/** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */
|
|
4
4
|
declare const assertNever: (value: never, message?: string) => never;
|
|
5
5
|
|
|
6
|
-
type GuardTarget
|
|
6
|
+
type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
|
|
7
|
+
type NarrowObject<T, P> = [Extract<T, P>] extends [never] ? T & P : Extract<T, P>;
|
|
8
|
+
/** Union of members narrowed by each pattern in `Ps` (multi-branch handlers). */
|
|
9
|
+
type NarrowUnion<T, Ps extends readonly unknown[]> = Narrow<T, Ps[number]>;
|
|
10
|
+
/** Narrows `input` when `pattern` is a shallow object, literal discriminant, or type predicate. */
|
|
11
|
+
type Narrow<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? T : NarrowObject<T, GuardTarget<P>> : P extends Record<string, unknown> ? NarrowObject<T, P> : T extends P ? Extract<T, P> : T;
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Union members of `T` ruled out by pattern `P`.
|
|
9
15
|
* Boolean guards (non-predicates) do not advance exhaustiveness — use `when(isX)` or `.otherwise()`.
|
|
10
16
|
*/
|
|
11
|
-
type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget
|
|
17
|
+
type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? never : Extract<T, GuardTarget<P>> : Extract<T, P>;
|
|
12
18
|
/** Union of cases already handled by prior branches. */
|
|
13
19
|
type HandledUnion<Handled extends readonly unknown[]> = Handled[number];
|
|
14
20
|
/** Input cases not yet covered by `.with` / `.withOneOf` / `.withEither`. */
|
|
@@ -25,13 +31,6 @@ type NonExhaustiveError<Remaining> = {
|
|
|
25
31
|
/** `.exhaustive()` return type — `NonExhaustiveError` when cases are missing. */
|
|
26
32
|
type ExhaustiveResult<T, Handled extends readonly unknown[], R, HasInput extends boolean> = IsExhaustive<T, Handled> extends true ? ExhaustiveOutput<R, HasInput, T> : NonExhaustiveError<RemainingCases<T, Handled>>;
|
|
27
33
|
|
|
28
|
-
type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
|
|
29
|
-
type NarrowObject<T, P> = [Extract<T, P>] extends [never] ? T & P : Extract<T, P>;
|
|
30
|
-
/** Union of members narrowed by each pattern in `Ps` (multi-branch handlers). */
|
|
31
|
-
type NarrowUnion<T, Ps extends readonly unknown[]> = Narrow<T, Ps[number]>;
|
|
32
|
-
/** Narrows `input` when `pattern` is a shallow object, literal discriminant, or type predicate. */
|
|
33
|
-
type Narrow<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? T : NarrowObject<T, GuardTarget<P>> : P extends Record<string, unknown> ? NarrowObject<T, P> : T extends P ? Extract<T, P> : T;
|
|
34
|
-
|
|
35
34
|
/**
|
|
36
35
|
* A pattern that matches a value of type `T`. One of:
|
|
37
36
|
*
|
|
@@ -57,6 +56,8 @@ type Case<T, R> = {
|
|
|
57
56
|
readonly test: (input: T) => boolean;
|
|
58
57
|
readonly run: (input: T) => R;
|
|
59
58
|
};
|
|
59
|
+
declare const NO_INPUT: unique symbol;
|
|
60
|
+
type NoInput = typeof NO_INPUT;
|
|
60
61
|
type NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;
|
|
61
62
|
type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;
|
|
62
63
|
/**
|
|
@@ -78,9 +79,11 @@ type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2
|
|
|
78
79
|
*/
|
|
79
80
|
declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
|
|
80
81
|
private readonly cases;
|
|
81
|
-
private readonly input
|
|
82
|
+
private readonly input;
|
|
82
83
|
readonly _handled: Handled;
|
|
83
|
-
constructor(cases
|
|
84
|
+
constructor(cases: readonly Case<T, unknown>[], input: T | NoInput, _handled?: Handled);
|
|
85
|
+
/** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
|
|
86
|
+
private addCase;
|
|
84
87
|
with<const P extends Pattern<T>, R2>(pattern: P, handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, ExhaustMatched<T, P>], Locked>;
|
|
85
88
|
/** One handler for several patterns (OR). Handler input is the union of narrowed members. */
|
|
86
89
|
withOneOf<const Ps extends readonly Pattern<T>[], R2>(patterns: Ps, handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, NarrowUnion<T, Ps>], Locked>;
|
|
@@ -90,10 +93,24 @@ declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handl
|
|
|
90
93
|
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
91
94
|
* Useful when branch return-type inference widens to a union narrower than
|
|
92
95
|
* the slot the match feeds into (e.g. `ReactNode`).
|
|
96
|
+
*
|
|
97
|
+
* Also the fix for literal-widening: when a handler returns an object whose
|
|
98
|
+
* field is a union literal, inference widens it (e.g. `{ mode: "native" }`
|
|
99
|
+
* becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type
|
|
100
|
+
* instead of sprinkling `as const`:
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* type State = { mode: "compat" | "native" };
|
|
105
|
+
* const next = match(flag)
|
|
106
|
+
* .returnType<State>() // handlers now checked against State
|
|
107
|
+
* .with("--native", () => ({ mode: "native" })) // no `as const` needed
|
|
108
|
+
* .otherwise(() => state);
|
|
109
|
+
* ```
|
|
93
110
|
*/
|
|
94
111
|
returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
|
|
95
112
|
/** Run on the value passed to {@link match}, or on `input` when curried. */
|
|
96
|
-
run(input
|
|
113
|
+
run(...input: readonly [input: T] | readonly []): R;
|
|
97
114
|
exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
|
|
98
115
|
otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
|
|
99
116
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,12 +3,18 @@ export { matchTag } from './tag.js';
|
|
|
3
3
|
/** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */
|
|
4
4
|
declare const assertNever: (value: never, message?: string) => never;
|
|
5
5
|
|
|
6
|
-
type GuardTarget
|
|
6
|
+
type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
|
|
7
|
+
type NarrowObject<T, P> = [Extract<T, P>] extends [never] ? T & P : Extract<T, P>;
|
|
8
|
+
/** Union of members narrowed by each pattern in `Ps` (multi-branch handlers). */
|
|
9
|
+
type NarrowUnion<T, Ps extends readonly unknown[]> = Narrow<T, Ps[number]>;
|
|
10
|
+
/** Narrows `input` when `pattern` is a shallow object, literal discriminant, or type predicate. */
|
|
11
|
+
type Narrow<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? T : NarrowObject<T, GuardTarget<P>> : P extends Record<string, unknown> ? NarrowObject<T, P> : T extends P ? Extract<T, P> : T;
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Union members of `T` ruled out by pattern `P`.
|
|
9
15
|
* Boolean guards (non-predicates) do not advance exhaustiveness — use `when(isX)` or `.otherwise()`.
|
|
10
16
|
*/
|
|
11
|
-
type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget
|
|
17
|
+
type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? never : Extract<T, GuardTarget<P>> : Extract<T, P>;
|
|
12
18
|
/** Union of cases already handled by prior branches. */
|
|
13
19
|
type HandledUnion<Handled extends readonly unknown[]> = Handled[number];
|
|
14
20
|
/** Input cases not yet covered by `.with` / `.withOneOf` / `.withEither`. */
|
|
@@ -25,13 +31,6 @@ type NonExhaustiveError<Remaining> = {
|
|
|
25
31
|
/** `.exhaustive()` return type — `NonExhaustiveError` when cases are missing. */
|
|
26
32
|
type ExhaustiveResult<T, Handled extends readonly unknown[], R, HasInput extends boolean> = IsExhaustive<T, Handled> extends true ? ExhaustiveOutput<R, HasInput, T> : NonExhaustiveError<RemainingCases<T, Handled>>;
|
|
27
33
|
|
|
28
|
-
type GuardTarget<F> = F extends (input: any) => input is infer U ? U : never;
|
|
29
|
-
type NarrowObject<T, P> = [Extract<T, P>] extends [never] ? T & P : Extract<T, P>;
|
|
30
|
-
/** Union of members narrowed by each pattern in `Ps` (multi-branch handlers). */
|
|
31
|
-
type NarrowUnion<T, Ps extends readonly unknown[]> = Narrow<T, Ps[number]>;
|
|
32
|
-
/** Narrows `input` when `pattern` is a shallow object, literal discriminant, or type predicate. */
|
|
33
|
-
type Narrow<T, P> = P extends (input: T) => boolean ? [GuardTarget<P>] extends [never] ? T : NarrowObject<T, GuardTarget<P>> : P extends Record<string, unknown> ? NarrowObject<T, P> : T extends P ? Extract<T, P> : T;
|
|
34
|
-
|
|
35
34
|
/**
|
|
36
35
|
* A pattern that matches a value of type `T`. One of:
|
|
37
36
|
*
|
|
@@ -57,6 +56,8 @@ type Case<T, R> = {
|
|
|
57
56
|
readonly test: (input: T) => boolean;
|
|
58
57
|
readonly run: (input: T) => R;
|
|
59
58
|
};
|
|
59
|
+
declare const NO_INPUT: unique symbol;
|
|
60
|
+
type NoInput = typeof NO_INPUT;
|
|
60
61
|
type NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;
|
|
61
62
|
type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;
|
|
62
63
|
/**
|
|
@@ -78,9 +79,11 @@ type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2
|
|
|
78
79
|
*/
|
|
79
80
|
declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
|
|
80
81
|
private readonly cases;
|
|
81
|
-
private readonly input
|
|
82
|
+
private readonly input;
|
|
82
83
|
readonly _handled: Handled;
|
|
83
|
-
constructor(cases
|
|
84
|
+
constructor(cases: readonly Case<T, unknown>[], input: T | NoInput, _handled?: Handled);
|
|
85
|
+
/** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
|
|
86
|
+
private addCase;
|
|
84
87
|
with<const P extends Pattern<T>, R2>(pattern: P, handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, ExhaustMatched<T, P>], Locked>;
|
|
85
88
|
/** One handler for several patterns (OR). Handler input is the union of narrowed members. */
|
|
86
89
|
withOneOf<const Ps extends readonly Pattern<T>[], R2>(patterns: Ps, handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, NarrowUnion<T, Ps>], Locked>;
|
|
@@ -90,10 +93,24 @@ declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handl
|
|
|
90
93
|
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
91
94
|
* Useful when branch return-type inference widens to a union narrower than
|
|
92
95
|
* the slot the match feeds into (e.g. `ReactNode`).
|
|
96
|
+
*
|
|
97
|
+
* Also the fix for literal-widening: when a handler returns an object whose
|
|
98
|
+
* field is a union literal, inference widens it (e.g. `{ mode: "native" }`
|
|
99
|
+
* becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type
|
|
100
|
+
* instead of sprinkling `as const`:
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* type State = { mode: "compat" | "native" };
|
|
105
|
+
* const next = match(flag)
|
|
106
|
+
* .returnType<State>() // handlers now checked against State
|
|
107
|
+
* .with("--native", () => ({ mode: "native" })) // no `as const` needed
|
|
108
|
+
* .otherwise(() => state);
|
|
109
|
+
* ```
|
|
93
110
|
*/
|
|
94
111
|
returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
|
|
95
112
|
/** Run on the value passed to {@link match}, or on `input` when curried. */
|
|
96
|
-
run(input
|
|
113
|
+
run(...input: readonly [input: T] | readonly []): R;
|
|
97
114
|
exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
|
|
98
115
|
otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
|
|
99
116
|
}
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ var matches = (input, pattern) => {
|
|
|
26
26
|
return true;
|
|
27
27
|
};
|
|
28
28
|
var NO_MATCH = /* @__PURE__ */ Symbol("@onrails/pattern/no-match");
|
|
29
|
+
var NO_INPUT = /* @__PURE__ */ Symbol("@onrails/pattern/no-input");
|
|
29
30
|
var runCases = (input, cases) => {
|
|
30
31
|
for (const c of cases) {
|
|
31
32
|
if (c.test(input)) {
|
|
@@ -39,7 +40,7 @@ var caseForPatterns = (patterns, handler) => ({
|
|
|
39
40
|
run: handler
|
|
40
41
|
});
|
|
41
42
|
var MatchBuilder = class _MatchBuilder {
|
|
42
|
-
constructor(cases
|
|
43
|
+
constructor(cases, input, _handled = []) {
|
|
43
44
|
this.cases = cases;
|
|
44
45
|
this.input = input;
|
|
45
46
|
this._handled = _handled;
|
|
@@ -47,21 +48,26 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
47
48
|
cases;
|
|
48
49
|
input;
|
|
49
50
|
_handled;
|
|
51
|
+
/** Appends a runtime case plus a phantom `H` entry on the handled tuple. */
|
|
52
|
+
addCase(next) {
|
|
53
|
+
return new _MatchBuilder(
|
|
54
|
+
[...this.cases, next],
|
|
55
|
+
this.input,
|
|
56
|
+
// Safe: _handled is a phantom tuple — only its type-level contents matter.
|
|
57
|
+
this._handled
|
|
58
|
+
);
|
|
59
|
+
}
|
|
50
60
|
with(pattern, handler) {
|
|
51
|
-
|
|
61
|
+
return this.addCase({
|
|
52
62
|
test: (input) => matches(input, pattern),
|
|
53
63
|
run: handler
|
|
54
|
-
};
|
|
55
|
-
return new _MatchBuilder([...this.cases, next], this.input, [
|
|
56
|
-
...this._handled
|
|
57
|
-
]);
|
|
64
|
+
});
|
|
58
65
|
}
|
|
59
66
|
/** One handler for several patterns (OR). Handler input is the union of narrowed members. */
|
|
60
67
|
withOneOf(patterns, handler) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
]);
|
|
68
|
+
return this.addCase(
|
|
69
|
+
caseForPatterns(patterns, handler)
|
|
70
|
+
);
|
|
65
71
|
}
|
|
66
72
|
/** `withOneOf` for exactly two patterns. */
|
|
67
73
|
withEither(pattern1, pattern2, handler) {
|
|
@@ -71,14 +77,28 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
71
77
|
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
72
78
|
* Useful when branch return-type inference widens to a union narrower than
|
|
73
79
|
* the slot the match feeds into (e.g. `ReactNode`).
|
|
80
|
+
*
|
|
81
|
+
* Also the fix for literal-widening: when a handler returns an object whose
|
|
82
|
+
* field is a union literal, inference widens it (e.g. `{ mode: "native" }`
|
|
83
|
+
* becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type
|
|
84
|
+
* instead of sprinkling `as const`:
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* type State = { mode: "compat" | "native" };
|
|
89
|
+
* const next = match(flag)
|
|
90
|
+
* .returnType<State>() // handlers now checked against State
|
|
91
|
+
* .with("--native", () => ({ mode: "native" })) // no `as const` needed
|
|
92
|
+
* .otherwise(() => state);
|
|
93
|
+
* ```
|
|
74
94
|
*/
|
|
75
95
|
returnType() {
|
|
76
96
|
return new _MatchBuilder(this.cases, this.input, this._handled);
|
|
77
97
|
}
|
|
78
98
|
/** Run on the value passed to {@link match}, or on `input` when curried. */
|
|
79
|
-
run(input) {
|
|
80
|
-
const value = input
|
|
81
|
-
if (value ===
|
|
99
|
+
run(...input) {
|
|
100
|
+
const value = input.length === 1 ? input[0] : this.input;
|
|
101
|
+
if (value === NO_INPUT) {
|
|
82
102
|
throw new Error("match.run: no input value");
|
|
83
103
|
}
|
|
84
104
|
const out = runCases(value, this.cases);
|
|
@@ -88,24 +108,18 @@ var MatchBuilder = class _MatchBuilder {
|
|
|
88
108
|
return out;
|
|
89
109
|
}
|
|
90
110
|
exhaustive() {
|
|
91
|
-
|
|
92
|
-
return this.run();
|
|
93
|
-
}
|
|
94
|
-
return ((value) => this.run(value));
|
|
111
|
+
return this.input === NO_INPUT ? (value) => this.run(value) : this.run();
|
|
95
112
|
}
|
|
96
113
|
otherwise(handler) {
|
|
97
114
|
const runWithFallback = (value) => {
|
|
98
115
|
const out = runCases(value, this.cases);
|
|
99
116
|
return out === NO_MATCH ? handler(value) : out;
|
|
100
117
|
};
|
|
101
|
-
|
|
102
|
-
return runWithFallback(this.input);
|
|
103
|
-
}
|
|
104
|
-
return runWithFallback;
|
|
118
|
+
return this.input === NO_INPUT ? runWithFallback : runWithFallback(this.input);
|
|
105
119
|
}
|
|
106
120
|
};
|
|
107
|
-
function match(input) {
|
|
108
|
-
return new MatchBuilder([], input);
|
|
121
|
+
function match(...input) {
|
|
122
|
+
return input.length === 1 ? new MatchBuilder([], input[0]) : new MatchBuilder([], NO_INPUT);
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
// src/tag.ts
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/assert.ts","../src/match.ts","../src/tag.ts","../src/when.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */\nexport const assertNever = (value: never, message = \"Unreachable\"): never => {\n throw new Error(`${message}: ${String(value)}`);\n};\n","import type { ExhaustiveResult, ExhaustMatched } from \"./exhaustive.js\";\nimport type { Narrow, NarrowUnion } from \"./narrow.js\";\n\n/**\n * A pattern that matches a value of type `T`. One of:\n *\n * - **Literal** — a primitive (`\"a\"`, `42`, `true`) matched by `===`.\n * - **Object pattern** — a `Partial<T>` of shallow key/value pairs that\n * must all match by strict equality. Only available when `T` is an\n * object type.\n * - **Guard** — a function `(input: T) => boolean`. Type predicates\n * (`(x): x is U`) narrow the handler input via {@link Narrow}.\n *\n * @example\n * ```ts\n * type Event = { kind: \"click\"; x: number } | { kind: \"key\"; code: string };\n *\n * const p1: Pattern<Event> = { kind: \"click\" }; // object pattern\n * const p2: Pattern<Event> = (e) => e.kind === \"key\"; // guard\n * ```\n */\nexport type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;\n\n// Shallow partial of `T`; restricted to object types so primitive `T`\n// (e.g. `number | string`) does not pick up `Record<string, unknown>`\n// as a candidate pattern and pollute `Narrow<T, P>` via distribution.\ntype ObjectPattern<T> = T extends object ? Partial<T> : never;\n\ntype PatternGuard<T> = (input: T) => boolean;\n\ntype Case<T, R> = {\n readonly test: (input: T) => boolean;\n readonly run: (input: T) => R;\n};\n\nconst isGuard = <T>(pattern: Pattern<T>): pattern is PatternGuard<T> =>\n typeof pattern === \"function\";\n\nconst isPatternObject = (pattern: unknown): pattern is Record<string, unknown> =>\n typeof pattern === \"object\" && pattern !== null;\n\nconst matches = <T>(input: T, pattern: Pattern<T>): boolean => {\n if (isGuard(pattern)) {\n return pattern(input);\n }\n if (!isPatternObject(pattern)) {\n return input === pattern;\n }\n if (typeof input !== \"object\" || input === null) {\n return false;\n }\n const record = input as Record<string, unknown>;\n const patternRecord = pattern as Record<string, unknown>;\n for (const key of Object.keys(patternRecord)) {\n if (record[key] !== patternRecord[key]) {\n return false;\n }\n }\n return true;\n};\n\n// Sentinel for \"no case matched\". Distinct from a handler legitimately\n// returning `undefined` (e.g. side-effect-only handlers), which would\n// otherwise be misreported as non-exhaustive.\nconst NO_MATCH = Symbol(\"@onrails/pattern/no-match\");\ntype NoMatch = typeof NO_MATCH;\n\nconst runCases = <T, R>(input: T, cases: readonly Case<T, R>[]): R | NoMatch => {\n for (const c of cases) {\n if (c.test(input)) {\n return c.run(input);\n }\n }\n return NO_MATCH;\n};\n\nconst caseForPatterns = <T, R>(\n patterns: readonly Pattern<T>[],\n handler: (input: T) => R,\n): Case<T, R> => ({\n test: (input) => patterns.some((pattern) => matches(input, pattern)),\n run: handler,\n});\n\n// Result-type accumulator: stays at `R` when the result type is locked\n// (after `returnType<R>()`), otherwise widens with each `.with`.\ntype NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;\n\n// Handler return-type constraint: when locked, every handler must return `R`;\n// when open, the handler can return any type and the result widens to include it.\ntype HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;\n\n/**\n * Fluent builder for {@link match}. Tracks the handled cases at the type\n * level so `.exhaustive()` only compiles when every union member is covered.\n *\n * The `Locked` phantom flag (set via `.returnType<R>()`) constrains every\n * subsequent handler to return `R`, useful when branch return-type\n * inference would widen to a union narrower than the target slot\n * (e.g. `ReactNode`, an API DTO).\n *\n * @example\n * ```ts\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport class MatchBuilder<\n T,\n R = never,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n Locked extends boolean = false,\n> {\n constructor(\n private readonly cases: readonly Case<T, unknown>[] = [],\n private readonly input?: T,\n // Safe: phantom tuple — only used for compile-time exhaustiveness tracking.\n readonly _handled: Handled = [] as unknown as Handled,\n ) {}\n\n with<const P extends Pattern<T>, R2>(\n pattern: P,\n handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, ExhaustMatched<T, P>],\n Locked\n > {\n const next: Case<T, unknown> = {\n test: (input) => matches(input, pattern),\n run: handler as (input: T) => unknown,\n };\n return new MatchBuilder([...this.cases, next], this.input, [\n ...this._handled,\n ] as unknown as readonly [...Handled, ExhaustMatched<T, P>]);\n }\n\n /** One handler for several patterns (OR). Handler input is the union of narrowed members. */\n withOneOf<const Ps extends readonly Pattern<T>[], R2>(\n patterns: Ps,\n handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, Ps>],\n Locked\n > {\n const next = caseForPatterns(patterns, handler as (input: T) => unknown);\n return new MatchBuilder([...this.cases, next], this.input, [\n ...this._handled,\n ] as unknown as readonly [...Handled, NarrowUnion<T, Ps>]);\n }\n\n /** `withOneOf` for exactly two patterns. */\n withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(\n pattern1: P1,\n pattern2: P2,\n handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>],\n Locked\n > {\n return this.withOneOf([pattern1, pattern2], handler);\n }\n\n /**\n * Lock the result type. All subsequent `.with()` handlers must return `R2`.\n * Useful when branch return-type inference widens to a union narrower than\n * the slot the match feeds into (e.g. `ReactNode`).\n */\n returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true> {\n return new MatchBuilder<T, R2, HasInput, Handled, true>(this.cases, this.input, this._handled);\n }\n\n /** Run on the value passed to {@link match}, or on `input` when curried. */\n run(input?: T): R {\n const value = input ?? this.input;\n if (value === undefined) {\n throw new Error(\"match.run: no input value\");\n }\n const out = runCases(value, this.cases);\n if (out === NO_MATCH) {\n throw new Error(\"Non-exhaustive match: no case matched input\");\n }\n return out as R;\n }\n\n exhaustive(): ExhaustiveResult<T, Handled, R, HasInput> {\n if (this.input !== undefined) {\n return this.run() as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n return ((value: T) => this.run(value)) as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n\n otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R {\n const runWithFallback = (value: T): R => {\n const out = runCases(value, this.cases);\n return out === NO_MATCH ? handler(value) : (out as R);\n };\n if (this.input !== undefined) {\n return runWithFallback(this.input) as HasInput extends true ? R : (input: T) => R;\n }\n return runWithFallback as HasInput extends true ? R : (input: T) => R;\n }\n}\n\n/**\n * Result-locked variant of {@link MatchBuilder}. Constructed via\n * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`\n * with the `Locked` phantom flag set — kept for backwards compatibility.\n */\nexport type LockedMatchBuilder<\n T,\n R,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n> = MatchBuilder<T, R, HasInput, Handled, true>;\n\n/**\n * Start a pattern-matching expression. Two call shapes:\n *\n * - `match(value)` — data-first; subsequent `.exhaustive()` or\n * `.otherwise(fn)` runs immediately against `value`.\n * - `match<T>()` — curried; subsequent `.exhaustive()` returns a function\n * `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.\n *\n * @example\n * ```ts\n * // Data-first\n * const out = match({ kind: \"click\", x: 1, y: 2 } as Event)\n * .with({ kind: \"click\" }, (e) => e.x + e.y)\n * .with({ kind: \"key\" }, () => -1)\n * .exhaustive();\n *\n * // Curried — build a reusable matcher\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport function match<T>(input: T): MatchBuilder<T, never, true>;\nexport function match<T = never>(): MatchBuilder<T, never, false>;\nexport function match<T>(input?: T): MatchBuilder<T, never, boolean> {\n return new MatchBuilder([], input);\n}\n","type Tagged = { readonly _tag: string };\n\ntype TagHandlers<T extends Tagged, R> = {\n [K in T[\"_tag\"]]: (value: Extract<T, { _tag: K }>) => R;\n};\n\n/**\n * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style unions.\n */\nexport const matchTag = <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>): R => {\n const tag = value._tag as T[\"_tag\"];\n const handler = handlers[tag] as (value: Extract<T, { _tag: typeof tag }>) => R;\n return handler(value as Extract<T, { _tag: typeof tag }>);\n};\n","import type { Pattern } from \"./match.js\";\n\n/**\n * Guard pattern for {@link match}.with.\n *\n * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate\n * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's\n * input is narrowed to the predicate's target type via {@link Narrow}.\n */\nexport function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T>;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T> {\n return guard;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/assert.ts","../src/match.ts","../src/tag.ts","../src/when.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */\nexport const assertNever = (value: never, message = \"Unreachable\"): never => {\n throw new Error(`${message}: ${String(value)}`);\n};\n","import type { ExhaustiveResult, ExhaustMatched } from \"./exhaustive.js\";\nimport type { Narrow, NarrowUnion } from \"./narrow.js\";\n\n/**\n * A pattern that matches a value of type `T`. One of:\n *\n * - **Literal** — a primitive (`\"a\"`, `42`, `true`) matched by `===`.\n * - **Object pattern** — a `Partial<T>` of shallow key/value pairs that\n * must all match by strict equality. Only available when `T` is an\n * object type.\n * - **Guard** — a function `(input: T) => boolean`. Type predicates\n * (`(x): x is U`) narrow the handler input via {@link Narrow}.\n *\n * @example\n * ```ts\n * type Event = { kind: \"click\"; x: number } | { kind: \"key\"; code: string };\n *\n * const p1: Pattern<Event> = { kind: \"click\" }; // object pattern\n * const p2: Pattern<Event> = (e) => e.kind === \"key\"; // guard\n * ```\n */\nexport type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;\n\n// Shallow partial of `T`; restricted to object types so primitive `T`\n// (e.g. `number | string`) does not pick up `Record<string, unknown>`\n// as a candidate pattern and pollute `Narrow<T, P>` via distribution.\ntype ObjectPattern<T> = T extends object ? Partial<T> : never;\n\ntype PatternGuard<T> = (input: T) => boolean;\n\ntype Case<T, R> = {\n readonly test: (input: T) => boolean;\n readonly run: (input: T) => R;\n};\n\nconst isGuard = <T>(pattern: Pattern<T>): pattern is PatternGuard<T> =>\n typeof pattern === \"function\";\n\nconst isPatternObject = (pattern: unknown): pattern is Record<string, unknown> =>\n typeof pattern === \"object\" && pattern !== null;\n\nconst matches = <T>(input: T, pattern: Pattern<T>): boolean => {\n if (isGuard(pattern)) {\n return pattern(input);\n }\n if (!isPatternObject(pattern)) {\n return input === pattern;\n }\n if (typeof input !== \"object\" || input === null) {\n return false;\n }\n const record = input as Record<string, unknown>;\n const patternRecord = pattern as Record<string, unknown>;\n for (const key of Object.keys(patternRecord)) {\n if (record[key] !== patternRecord[key]) {\n return false;\n }\n }\n return true;\n};\n\n// Sentinel for \"no case matched\". Distinct from a handler legitimately\n// returning `undefined` (e.g. side-effect-only handlers), which would\n// otherwise be misreported as non-exhaustive.\nconst NO_MATCH = Symbol(\"@onrails/pattern/no-match\");\ntype NoMatch = typeof NO_MATCH;\n\n// Sentinel for \"curried matcher, no captured input\". Distinct from a\n// data-first `match(undefined)` / `match(null)`, which must run immediately\n// like any other value instead of silently degrading to curried mode.\nconst NO_INPUT = Symbol(\"@onrails/pattern/no-input\");\ntype NoInput = typeof NO_INPUT;\n\nconst runCases = <T, R>(input: T, cases: readonly Case<T, R>[]): R | NoMatch => {\n for (const c of cases) {\n if (c.test(input)) {\n return c.run(input);\n }\n }\n return NO_MATCH;\n};\n\nconst caseForPatterns = <T, R>(\n patterns: readonly Pattern<T>[],\n handler: (input: T) => R,\n): Case<T, R> => ({\n test: (input) => patterns.some((pattern) => matches(input, pattern)),\n run: handler,\n});\n\n// Result-type accumulator: stays at `R` when the result type is locked\n// (after `returnType<R>()`), otherwise widens with each `.with`.\ntype NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;\n\n// Handler return-type constraint: when locked, every handler must return `R`;\n// when open, the handler can return any type and the result widens to include it.\ntype HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;\n\n/**\n * Fluent builder for {@link match}. Tracks the handled cases at the type\n * level so `.exhaustive()` only compiles when every union member is covered.\n *\n * The `Locked` phantom flag (set via `.returnType<R>()`) constrains every\n * subsequent handler to return `R`, useful when branch return-type\n * inference would widen to a union narrower than the target slot\n * (e.g. `ReactNode`, an API DTO).\n *\n * @example\n * ```ts\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport class MatchBuilder<\n T,\n R = never,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n Locked extends boolean = false,\n> {\n constructor(\n private readonly cases: readonly Case<T, unknown>[],\n // No default: `= NO_INPUT` would swallow a data-first `undefined` input,\n // because JS applies parameter defaults to explicitly-passed undefined.\n private readonly input: T | NoInput,\n // Safe: phantom tuple — only used for compile-time exhaustiveness tracking.\n readonly _handled: Handled = [] as unknown as Handled,\n ) {}\n\n /** Appends a runtime case plus a phantom `H` entry on the handled tuple. */\n private addCase<R2, H>(\n next: Case<T, unknown>,\n ): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, H], Locked> {\n return new MatchBuilder(\n [...this.cases, next],\n this.input,\n // Safe: _handled is a phantom tuple — only its type-level contents matter.\n this._handled as unknown as readonly [...Handled, H],\n );\n }\n\n with<const P extends Pattern<T>, R2>(\n pattern: P,\n handler: (input: Narrow<T, P>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, ExhaustMatched<T, P>],\n Locked\n > {\n return this.addCase<R2, ExhaustMatched<T, P>>({\n test: (input) => matches(input, pattern),\n run: handler as (input: T) => unknown,\n });\n }\n\n /** One handler for several patterns (OR). Handler input is the union of narrowed members. */\n withOneOf<const Ps extends readonly Pattern<T>[], R2>(\n patterns: Ps,\n handler: (input: NarrowUnion<T, Ps>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, Ps>],\n Locked\n > {\n return this.addCase<R2, NarrowUnion<T, Ps>>(\n caseForPatterns(patterns, handler as (input: T) => unknown),\n );\n }\n\n /** `withOneOf` for exactly two patterns. */\n withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(\n pattern1: P1,\n pattern2: P2,\n handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>,\n ): MatchBuilder<\n T,\n NextResult<Locked, R, R2>,\n HasInput,\n readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>],\n Locked\n > {\n return this.withOneOf([pattern1, pattern2], handler);\n }\n\n /**\n * Lock the result type. All subsequent `.with()` handlers must return `R2`.\n * Useful when branch return-type inference widens to a union narrower than\n * the slot the match feeds into (e.g. `ReactNode`).\n *\n * Also the fix for literal-widening: when a handler returns an object whose\n * field is a union literal, inference widens it (e.g. `{ mode: \"native\" }`\n * becomes `{ mode: string }`, breaking a `Mode` union). Lock the target type\n * instead of sprinkling `as const`:\n *\n * @example\n * ```ts\n * type State = { mode: \"compat\" | \"native\" };\n * const next = match(flag)\n * .returnType<State>() // handlers now checked against State\n * .with(\"--native\", () => ({ mode: \"native\" })) // no `as const` needed\n * .otherwise(() => state);\n * ```\n */\n returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true> {\n return new MatchBuilder<T, R2, HasInput, Handled, true>(this.cases, this.input, this._handled);\n }\n\n /** Run on the value passed to {@link match}, or on `input` when curried. */\n run(...input: readonly [input: T] | readonly []): R {\n const value = input.length === 1 ? input[0] : this.input;\n if (value === NO_INPUT) {\n throw new Error(\"match.run: no input value\");\n }\n const out = runCases(value, this.cases);\n if (out === NO_MATCH) {\n throw new Error(\"Non-exhaustive match: no case matched input\");\n }\n return out as R;\n }\n\n exhaustive(): ExhaustiveResult<T, Handled, R, HasInput> {\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? (value: T) => this.run(value) : this.run()\n ) as ExhaustiveResult<T, Handled, R, HasInput>;\n }\n\n otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R {\n const runWithFallback = (value: T): R => {\n const out = runCases(value, this.cases);\n return out === NO_MATCH ? handler(value) : (out as R);\n };\n // Safe: HasInput is true exactly when match(value) captured an input.\n return (\n this.input === NO_INPUT ? runWithFallback : runWithFallback(this.input)\n ) as HasInput extends true ? R : (input: T) => R;\n }\n}\n\n/**\n * Result-locked variant of {@link MatchBuilder}. Constructed via\n * `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`\n * with the `Locked` phantom flag set — kept for backwards compatibility.\n */\nexport type LockedMatchBuilder<\n T,\n R,\n HasInput extends boolean = false,\n Handled extends readonly unknown[] = [],\n> = MatchBuilder<T, R, HasInput, Handled, true>;\n\n/**\n * Start a pattern-matching expression. Two call shapes:\n *\n * - `match(value)` — data-first; subsequent `.exhaustive()` or\n * `.otherwise(fn)` runs immediately against `value`.\n * - `match<T>()` — curried; subsequent `.exhaustive()` returns a function\n * `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.\n *\n * @example\n * ```ts\n * // Data-first\n * const out = match({ kind: \"click\", x: 1, y: 2 } as Event)\n * .with({ kind: \"click\" }, (e) => e.x + e.y)\n * .with({ kind: \"key\" }, () => -1)\n * .exhaustive();\n *\n * // Curried — build a reusable matcher\n * const describe = match<Event>()\n * .with({ kind: \"click\" }, (e) => `click @ ${e.x},${e.y}`)\n * .with({ kind: \"key\" }, (e) => `key ${e.code}`)\n * .exhaustive();\n * ```\n */\nexport function match<T>(input: T): MatchBuilder<T, never, true>;\nexport function match<T = never>(): MatchBuilder<T, never, false>;\nexport function match<T>(\n ...input: readonly [input: T] | readonly []\n): MatchBuilder<T, never, boolean> {\n return input.length === 1\n ? new MatchBuilder<T, never, boolean>([], input[0])\n : new MatchBuilder<T, never, boolean>([], NO_INPUT);\n}\n","type Tagged = { readonly _tag: string };\n\ntype TagHandlers<T extends Tagged, R> = {\n [K in T[\"_tag\"]]: (value: Extract<T, { _tag: K }>) => R;\n};\n\n/**\n * Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style unions.\n */\nexport const matchTag = <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>): R => {\n const tag = value._tag as T[\"_tag\"];\n const handler = handlers[tag] as (value: Extract<T, { _tag: typeof tag }>) => R;\n return handler(value as Extract<T, { _tag: typeof tag }>);\n};\n","import type { Pattern } from \"./match.js\";\n\n/**\n * Guard pattern for {@link match}.with.\n *\n * Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate\n * guards (`(x): x is Foo => ...`). For type predicates, the matched handler's\n * input is narrowed to the predicate's target type via {@link Narrow}.\n */\nexport function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T>;\nexport function when<T>(guard: (input: T) => boolean): Pattern<T> {\n return guard;\n}\n"]}
|
package/package.json
CHANGED