@onrails/pattern 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/DESIGN.md +71 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/index.cjs +131 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +142 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/tag.cjs +12 -0
- package/dist/tag.cjs.map +1 -0
- package/dist/tag.d.cts +14 -0
- package/dist/tag.d.ts +14 -0
- package/dist/tag.js +10 -0
- package/dist/tag.js.map +1 -0
- package/package.json +69 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @onrails/pattern — design
|
|
2
|
+
|
|
3
|
+
Lightweight matching for **owned** tagged unions and finite domain states. Inspired by ts-pattern; aligned with `@onrails/result` DX.
|
|
4
|
+
|
|
5
|
+
## Goals
|
|
6
|
+
|
|
7
|
+
- **Smaller runtime** than ts-pattern — shallow object match, literal/primitive equality, guards
|
|
8
|
+
- **ts-pattern-shaped API**: `match(x).with(...).exhaustive()` / `.otherwise()`
|
|
9
|
+
- **`matchTag`** for `_tag` unions (`Ok` / `Err`, `Some` / `None`, domain ADTs)
|
|
10
|
+
- No `P.*` wildcard zoo in v1 — use `when(guard)` or `matchTag`
|
|
11
|
+
|
|
12
|
+
## Handler narrowing
|
|
13
|
+
|
|
14
|
+
`.with(pattern, handler)` narrows the handler's input via `Narrow<T, P>`:
|
|
15
|
+
|
|
16
|
+
- **Discriminated union**: `Extract<T, P>` picks the matching member.
|
|
17
|
+
- **Single object type**: falls back to intersection `T & P` so structural matches still narrow (e.g. `{ status: "failed" }` on a `Job` with `status: JobStatus`).
|
|
18
|
+
- **Type-predicate guard** (`(x): x is U`): narrows to `U` via `when(predicate)`.
|
|
19
|
+
- **Plain boolean guard** (`(x) => boolean`): leaves the handler input as `T`.
|
|
20
|
+
|
|
21
|
+
`Pattern<T>` admits object-shaped patterns only when `T extends object`. Primitive `T` (`number | string`) accepts literals and guards but not free-form objects — this prevents `Record<string, unknown>` distributing into `Narrow` and polluting the handler input type.
|
|
22
|
+
|
|
23
|
+
## Result-type seeding
|
|
24
|
+
|
|
25
|
+
`match(x).returnType<R>()` flips the builder's `Locked` phantom flag — the returned `MatchBuilder` (aliased as `LockedMatchBuilder<T, R, …>` for backwards compatibility) constrains every subsequent `.with()` handler to return `R`. Use when branch return-type inference widens to a union narrower than the slot the match feeds into (`ReactNode`, an API DTO, etc.).
|
|
26
|
+
|
|
27
|
+
## Type tests
|
|
28
|
+
|
|
29
|
+
`test/types.spec.ts` uses `ts-expect` (`expectType` + `TypeEqual`) — same approach as styled-cva.
|
|
30
|
+
|
|
31
|
+
## Compile-time exhaustiveness
|
|
32
|
+
|
|
33
|
+
`.exhaustive()` is only typed when every member of union `T` is covered by prior `.with` / `.withOneOf` / `.withEither` branches. The builder tracks `Matched`; `RemainingCases<T, Matched>` must be `never`.
|
|
34
|
+
|
|
35
|
+
- Object/literal patterns use `Extract<T, P>`.
|
|
36
|
+
- `when(predicate)` with a **type predicate** narrows like `.with`; plain boolean guards do **not** advance exhaustiveness (use `.otherwise()` or add explicit branches).
|
|
37
|
+
- **Single object types** with an enum/status field (not a top-level union) are not proven exhaustive — model as a discriminated union or use `.otherwise()`.
|
|
38
|
+
|
|
39
|
+
Runtime still throws if a value slips through (e.g. unsound cast on input).
|
|
40
|
+
|
|
41
|
+
## Non-goals (v1)
|
|
42
|
+
|
|
43
|
+
- Deep/spread patterns, `P.select`, `P.not`, nested unwrapping
|
|
44
|
+
- Replacing `if` for two-branch checks or nullable guards
|
|
45
|
+
|
|
46
|
+
## Exports
|
|
47
|
+
|
|
48
|
+
| Subpath | Purpose |
|
|
49
|
+
|---------|---------|
|
|
50
|
+
| `@onrails/pattern` | `match`, `when`, `assertNever`, `MatchBuilder`, `LockedMatchBuilder`, `Pattern`, `Narrow`, `NarrowUnion`, `RemainingCases`, `IsExhaustive`, `NonExhaustiveError` |
|
|
51
|
+
| `@onrails/pattern/tag` | `matchTag` for `_tag` dispatch |
|
|
52
|
+
|
|
53
|
+
## Migration from ts-pattern
|
|
54
|
+
|
|
55
|
+
| ts-pattern | @onrails/pattern |
|
|
56
|
+
|------------|------------------|
|
|
57
|
+
| `match(x).with({ type: "a" }, fn).exhaustive()` | Same |
|
|
58
|
+
| `match(x).with("a", fn).exhaustive()` | Same (primitive / literal union) |
|
|
59
|
+
| `match(x).with(p1, p2, fn)` (multi-pattern) | `.withOneOf([p1, p2], fn)` or `.withEither(p1, p2, fn)` |
|
|
60
|
+
| `P.when(fn)` | `when(fn)` (preserves type-predicate narrowing) |
|
|
61
|
+
| `match(x).returnType<R>()` | Same |
|
|
62
|
+
| `P._` / nested selects | Not v1 — keep ts-pattern or refactor to `matchTag` |
|
|
63
|
+
|
|
64
|
+
## Multi-pattern
|
|
65
|
+
|
|
66
|
+
`.withOneOf([p1, p2, …], handler)` registers one case whose test ORs the patterns. Handler input is `NarrowUnion<T, Ps>` (union of per-pattern narrowings). `.withEither(p1, p2, handler)` is sugar for two patterns.
|
|
67
|
+
|
|
68
|
+
## Deferred
|
|
69
|
+
|
|
70
|
+
- Variadic `.with(p1, p2, fn)` overload (ts-pattern arity style)
|
|
71
|
+
- `compat/ts-pattern` re-export or codemod
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alan R. Soares
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @onrails/pattern
|
|
2
|
+
|
|
3
|
+
Exhaustive matching for owned tagged unions — ts-pattern-shaped API, smaller runtime than full ts-pattern.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @onrails/pattern
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { match } from "@onrails/pattern";
|
|
15
|
+
|
|
16
|
+
type Status = "idle" | "running" | "done";
|
|
17
|
+
|
|
18
|
+
const label = (s: Status) =>
|
|
19
|
+
match(s)
|
|
20
|
+
.with("idle", () => "Idle")
|
|
21
|
+
.with("running", () => "Running")
|
|
22
|
+
.with("done", () => "Done")
|
|
23
|
+
.exhaustive();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Object discriminant (shallow key match):
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { match } from "@onrails/pattern";
|
|
30
|
+
|
|
31
|
+
const render = (event: { type: "msg"; text: string } | { type: "err"; code: number }) =>
|
|
32
|
+
match(event)
|
|
33
|
+
.with({ type: "msg" }, (e) => e.text) // `e` is narrowed — no cast
|
|
34
|
+
.with({ type: "err" }, (e) => String(e.code))
|
|
35
|
+
.exhaustive();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Type-predicate guards via `when`:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { match, when } from "@onrails/pattern";
|
|
42
|
+
|
|
43
|
+
type Event = { type: "msg"; text: string } | { type: "err"; code: number };
|
|
44
|
+
const isMsg = (e: Event): e is Extract<Event, { type: "msg" }> => e.type === "msg";
|
|
45
|
+
|
|
46
|
+
const render = (e: Event) =>
|
|
47
|
+
match(e)
|
|
48
|
+
.with(when(isMsg), (msg) => msg.text) // `msg` narrowed to { type: "msg"; text: string }
|
|
49
|
+
.otherwise(() => "");
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Lock the result type with `returnType<R>()` when branch inference would otherwise widen too narrowly:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import type { ReactNode } from "react";
|
|
56
|
+
import { match } from "@onrails/pattern";
|
|
57
|
+
|
|
58
|
+
const render = (p: Part): ReactNode =>
|
|
59
|
+
match(p)
|
|
60
|
+
.returnType<ReactNode>() // every `.with` handler must return ReactNode
|
|
61
|
+
.with({ type: "text" }, (t) => <Text>{t.text}</Text>)
|
|
62
|
+
.with({ type: "image" }, (i) => <Image src={i.src} />)
|
|
63
|
+
.otherwise(() => null);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`_tag` unions (`@onrails/result`, `@onrails/maybe`):
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { matchTag } from "@onrails/pattern/tag";
|
|
70
|
+
import { isOk, type Result } from "@onrails/result";
|
|
71
|
+
|
|
72
|
+
const show = <T, E>(r: Result<T, E>) =>
|
|
73
|
+
matchTag(r, {
|
|
74
|
+
Ok: (v) => v.value,
|
|
75
|
+
Err: (e) => e.error,
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Subpaths
|
|
80
|
+
|
|
81
|
+
| Path | Contents |
|
|
82
|
+
|------|----------|
|
|
83
|
+
| `@onrails/pattern` | `match`, `when`, `assertNever`, `MatchBuilder`, `LockedMatchBuilder` |
|
|
84
|
+
| `@onrails/pattern/tag` | `matchTag` |
|
|
85
|
+
|
|
86
|
+
See [DESIGN.md](./DESIGN.md).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/assert.ts
|
|
4
|
+
var assertNever = (value, message = "Unreachable") => {
|
|
5
|
+
throw new Error(`${message}: ${String(value)}`);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// src/match.ts
|
|
9
|
+
var isGuard = (pattern) => typeof pattern === "function";
|
|
10
|
+
var isPatternObject = (pattern) => typeof pattern === "object" && pattern !== null;
|
|
11
|
+
var matches = (input, pattern) => {
|
|
12
|
+
if (isGuard(pattern)) {
|
|
13
|
+
return pattern(input);
|
|
14
|
+
}
|
|
15
|
+
if (!isPatternObject(pattern)) {
|
|
16
|
+
return input === pattern;
|
|
17
|
+
}
|
|
18
|
+
if (typeof input !== "object" || input === null) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const record = input;
|
|
22
|
+
const patternRecord = pattern;
|
|
23
|
+
for (const key of Object.keys(patternRecord)) {
|
|
24
|
+
if (record[key] !== patternRecord[key]) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
var NO_MATCH = /* @__PURE__ */ Symbol("@onrails/pattern/no-match");
|
|
31
|
+
var runCases = (input, cases) => {
|
|
32
|
+
for (const c of cases) {
|
|
33
|
+
if (c.test(input)) {
|
|
34
|
+
return c.run(input);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return NO_MATCH;
|
|
38
|
+
};
|
|
39
|
+
var caseForPatterns = (patterns, handler) => ({
|
|
40
|
+
test: (input) => patterns.some((pattern) => matches(input, pattern)),
|
|
41
|
+
run: handler
|
|
42
|
+
});
|
|
43
|
+
var MatchBuilder = class _MatchBuilder {
|
|
44
|
+
constructor(cases = [], input, _handled = []) {
|
|
45
|
+
this.cases = cases;
|
|
46
|
+
this.input = input;
|
|
47
|
+
this._handled = _handled;
|
|
48
|
+
}
|
|
49
|
+
cases;
|
|
50
|
+
input;
|
|
51
|
+
_handled;
|
|
52
|
+
with(pattern, handler) {
|
|
53
|
+
const next = {
|
|
54
|
+
test: (input) => matches(input, pattern),
|
|
55
|
+
run: handler
|
|
56
|
+
};
|
|
57
|
+
return new _MatchBuilder([...this.cases, next], this.input, [
|
|
58
|
+
...this._handled
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
/** One handler for several patterns (OR). Handler input is the union of narrowed members. */
|
|
62
|
+
withOneOf(patterns, handler) {
|
|
63
|
+
const next = caseForPatterns(patterns, handler);
|
|
64
|
+
return new _MatchBuilder([...this.cases, next], this.input, [
|
|
65
|
+
...this._handled
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
/** `withOneOf` for exactly two patterns. */
|
|
69
|
+
withEither(pattern1, pattern2, handler) {
|
|
70
|
+
return this.withOneOf([pattern1, pattern2], handler);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
74
|
+
* Useful when branch return-type inference widens to a union narrower than
|
|
75
|
+
* the slot the match feeds into (e.g. `ReactNode`).
|
|
76
|
+
*/
|
|
77
|
+
returnType() {
|
|
78
|
+
return new _MatchBuilder(this.cases, this.input, this._handled);
|
|
79
|
+
}
|
|
80
|
+
/** Run on the value passed to {@link match}, or on `input` when curried. */
|
|
81
|
+
run(input) {
|
|
82
|
+
const value = input ?? this.input;
|
|
83
|
+
if (value === void 0) {
|
|
84
|
+
throw new Error("match.run: no input value");
|
|
85
|
+
}
|
|
86
|
+
const out = runCases(value, this.cases);
|
|
87
|
+
if (out === NO_MATCH) {
|
|
88
|
+
throw new Error("Non-exhaustive match: no case matched input");
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
exhaustive() {
|
|
93
|
+
if (this.input !== void 0) {
|
|
94
|
+
return this.run();
|
|
95
|
+
}
|
|
96
|
+
return ((value) => this.run(value));
|
|
97
|
+
}
|
|
98
|
+
otherwise(handler) {
|
|
99
|
+
const runWithFallback = (value) => {
|
|
100
|
+
const out = runCases(value, this.cases);
|
|
101
|
+
return out === NO_MATCH ? handler(value) : out;
|
|
102
|
+
};
|
|
103
|
+
if (this.input !== void 0) {
|
|
104
|
+
return runWithFallback(this.input);
|
|
105
|
+
}
|
|
106
|
+
return runWithFallback;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
function match(input) {
|
|
110
|
+
return new MatchBuilder([], input);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/tag.ts
|
|
114
|
+
var matchTag = (value, handlers) => {
|
|
115
|
+
const tag = value._tag;
|
|
116
|
+
const handler = handlers[tag];
|
|
117
|
+
return handler(value);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/when.ts
|
|
121
|
+
function when(guard) {
|
|
122
|
+
return guard;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
exports.MatchBuilder = MatchBuilder;
|
|
126
|
+
exports.assertNever = assertNever;
|
|
127
|
+
exports.match = match;
|
|
128
|
+
exports.matchTag = matchTag;
|
|
129
|
+
exports.when = when;
|
|
130
|
+
//# sourceMappingURL=index.cjs.map
|
|
131
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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.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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export { matchTag } from './tag.cjs';
|
|
2
|
+
|
|
3
|
+
/** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */
|
|
4
|
+
declare const assertNever: (value: never, message?: string) => never;
|
|
5
|
+
|
|
6
|
+
type GuardTarget$1<F> = F extends (input: any) => input is infer U ? U : never;
|
|
7
|
+
/**
|
|
8
|
+
* Union members of `T` ruled out by pattern `P`.
|
|
9
|
+
* Boolean guards (non-predicates) do not advance exhaustiveness — use `when(isX)` or `.otherwise()`.
|
|
10
|
+
*/
|
|
11
|
+
type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget$1<P>] extends [never] ? never : Extract<T, GuardTarget$1<P>> : Extract<T, P>;
|
|
12
|
+
/** Union of cases already handled by prior branches. */
|
|
13
|
+
type HandledUnion<Handled extends readonly unknown[]> = Handled[number];
|
|
14
|
+
/** Input cases not yet covered by `.with` / `.withOneOf` / `.withEither`. */
|
|
15
|
+
type RemainingCases<T, Handled extends readonly unknown[]> = Exclude<T, HandledUnion<Handled>>;
|
|
16
|
+
type IsExhaustive<T, Handled extends readonly unknown[]> = [
|
|
17
|
+
RemainingCases<T, Handled>
|
|
18
|
+
] extends [never] ? true : false;
|
|
19
|
+
type ExhaustiveOutput<R, HasInput extends boolean, T> = HasInput extends true ? R : (input: T) => R;
|
|
20
|
+
/** Returned by `.exhaustive()` when union cases are still missing (compile-time error). */
|
|
21
|
+
type NonExhaustiveError<Remaining> = {
|
|
22
|
+
readonly __nonExhaustive: "Add .with() branches for remaining cases";
|
|
23
|
+
readonly remaining: Remaining;
|
|
24
|
+
};
|
|
25
|
+
/** `.exhaustive()` return type — `NonExhaustiveError` when cases are missing. */
|
|
26
|
+
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
|
+
|
|
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
|
+
/**
|
|
36
|
+
* A pattern that matches a value of type `T`. One of:
|
|
37
|
+
*
|
|
38
|
+
* - **Literal** — a primitive (`"a"`, `42`, `true`) matched by `===`.
|
|
39
|
+
* - **Object pattern** — a `Partial<T>` of shallow key/value pairs that
|
|
40
|
+
* must all match by strict equality. Only available when `T` is an
|
|
41
|
+
* object type.
|
|
42
|
+
* - **Guard** — a function `(input: T) => boolean`. Type predicates
|
|
43
|
+
* (`(x): x is U`) narrow the handler input via {@link Narrow}.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* type Event = { kind: "click"; x: number } | { kind: "key"; code: string };
|
|
48
|
+
*
|
|
49
|
+
* const p1: Pattern<Event> = { kind: "click" }; // object pattern
|
|
50
|
+
* const p2: Pattern<Event> = (e) => e.kind === "key"; // guard
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;
|
|
54
|
+
type ObjectPattern<T> = T extends object ? Partial<T> : never;
|
|
55
|
+
type PatternGuard<T> = (input: T) => boolean;
|
|
56
|
+
type Case<T, R> = {
|
|
57
|
+
readonly test: (input: T) => boolean;
|
|
58
|
+
readonly run: (input: T) => R;
|
|
59
|
+
};
|
|
60
|
+
type NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;
|
|
61
|
+
type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;
|
|
62
|
+
/**
|
|
63
|
+
* Fluent builder for {@link match}. Tracks the handled cases at the type
|
|
64
|
+
* level so `.exhaustive()` only compiles when every union member is covered.
|
|
65
|
+
*
|
|
66
|
+
* The `Locked` phantom flag (set via `.returnType<R>()`) constrains every
|
|
67
|
+
* subsequent handler to return `R`, useful when branch return-type
|
|
68
|
+
* inference would widen to a union narrower than the target slot
|
|
69
|
+
* (e.g. `ReactNode`, an API DTO).
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const describe = match<Event>()
|
|
74
|
+
* .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
|
|
75
|
+
* .with({ kind: "key" }, (e) => `key ${e.code}`)
|
|
76
|
+
* .exhaustive();
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
|
|
80
|
+
private readonly cases;
|
|
81
|
+
private readonly input?;
|
|
82
|
+
readonly _handled: Handled;
|
|
83
|
+
constructor(cases?: readonly Case<T, unknown>[], input?: T | undefined, _handled?: Handled);
|
|
84
|
+
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
|
+
/** One handler for several patterns (OR). Handler input is the union of narrowed members. */
|
|
86
|
+
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>;
|
|
87
|
+
/** `withOneOf` for exactly two patterns. */
|
|
88
|
+
withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(pattern1: P1, pattern2: P2, handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>], Locked>;
|
|
89
|
+
/**
|
|
90
|
+
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
91
|
+
* Useful when branch return-type inference widens to a union narrower than
|
|
92
|
+
* the slot the match feeds into (e.g. `ReactNode`).
|
|
93
|
+
*/
|
|
94
|
+
returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
|
|
95
|
+
/** Run on the value passed to {@link match}, or on `input` when curried. */
|
|
96
|
+
run(input?: T): R;
|
|
97
|
+
exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
|
|
98
|
+
otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Result-locked variant of {@link MatchBuilder}. Constructed via
|
|
102
|
+
* `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`
|
|
103
|
+
* with the `Locked` phantom flag set — kept for backwards compatibility.
|
|
104
|
+
*/
|
|
105
|
+
type LockedMatchBuilder<T, R, HasInput extends boolean = false, Handled extends readonly unknown[] = []> = MatchBuilder<T, R, HasInput, Handled, true>;
|
|
106
|
+
/**
|
|
107
|
+
* Start a pattern-matching expression. Two call shapes:
|
|
108
|
+
*
|
|
109
|
+
* - `match(value)` — data-first; subsequent `.exhaustive()` or
|
|
110
|
+
* `.otherwise(fn)` runs immediately against `value`.
|
|
111
|
+
* - `match<T>()` — curried; subsequent `.exhaustive()` returns a function
|
|
112
|
+
* `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* // Data-first
|
|
117
|
+
* const out = match({ kind: "click", x: 1, y: 2 } as Event)
|
|
118
|
+
* .with({ kind: "click" }, (e) => e.x + e.y)
|
|
119
|
+
* .with({ kind: "key" }, () => -1)
|
|
120
|
+
* .exhaustive();
|
|
121
|
+
*
|
|
122
|
+
* // Curried — build a reusable matcher
|
|
123
|
+
* const describe = match<Event>()
|
|
124
|
+
* .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
|
|
125
|
+
* .with({ kind: "key" }, (e) => `key ${e.code}`)
|
|
126
|
+
* .exhaustive();
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
declare function match<T>(input: T): MatchBuilder<T, never, true>;
|
|
130
|
+
declare function match<T = never>(): MatchBuilder<T, never, false>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Guard pattern for {@link match}.with.
|
|
134
|
+
*
|
|
135
|
+
* Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate
|
|
136
|
+
* guards (`(x): x is Foo => ...`). For type predicates, the matched handler's
|
|
137
|
+
* input is narrowed to the predicate's target type via {@link Narrow}.
|
|
138
|
+
*/
|
|
139
|
+
declare function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;
|
|
140
|
+
declare function when<T>(guard: (input: T) => boolean): Pattern<T>;
|
|
141
|
+
|
|
142
|
+
export { type ExhaustMatched, type ExhaustiveResult, type HandledUnion, type IsExhaustive, type LockedMatchBuilder, MatchBuilder, type Narrow, type NarrowUnion, type NonExhaustiveError, type Pattern, type RemainingCases, assertNever, match, when };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export { matchTag } from './tag.js';
|
|
2
|
+
|
|
3
|
+
/** Use in `default` branches after manual narrowing — compile-time exhaustiveness check. */
|
|
4
|
+
declare const assertNever: (value: never, message?: string) => never;
|
|
5
|
+
|
|
6
|
+
type GuardTarget$1<F> = F extends (input: any) => input is infer U ? U : never;
|
|
7
|
+
/**
|
|
8
|
+
* Union members of `T` ruled out by pattern `P`.
|
|
9
|
+
* Boolean guards (non-predicates) do not advance exhaustiveness — use `when(isX)` or `.otherwise()`.
|
|
10
|
+
*/
|
|
11
|
+
type ExhaustMatched<T, P> = P extends (input: T) => boolean ? [GuardTarget$1<P>] extends [never] ? never : Extract<T, GuardTarget$1<P>> : Extract<T, P>;
|
|
12
|
+
/** Union of cases already handled by prior branches. */
|
|
13
|
+
type HandledUnion<Handled extends readonly unknown[]> = Handled[number];
|
|
14
|
+
/** Input cases not yet covered by `.with` / `.withOneOf` / `.withEither`. */
|
|
15
|
+
type RemainingCases<T, Handled extends readonly unknown[]> = Exclude<T, HandledUnion<Handled>>;
|
|
16
|
+
type IsExhaustive<T, Handled extends readonly unknown[]> = [
|
|
17
|
+
RemainingCases<T, Handled>
|
|
18
|
+
] extends [never] ? true : false;
|
|
19
|
+
type ExhaustiveOutput<R, HasInput extends boolean, T> = HasInput extends true ? R : (input: T) => R;
|
|
20
|
+
/** Returned by `.exhaustive()` when union cases are still missing (compile-time error). */
|
|
21
|
+
type NonExhaustiveError<Remaining> = {
|
|
22
|
+
readonly __nonExhaustive: "Add .with() branches for remaining cases";
|
|
23
|
+
readonly remaining: Remaining;
|
|
24
|
+
};
|
|
25
|
+
/** `.exhaustive()` return type — `NonExhaustiveError` when cases are missing. */
|
|
26
|
+
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
|
+
|
|
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
|
+
/**
|
|
36
|
+
* A pattern that matches a value of type `T`. One of:
|
|
37
|
+
*
|
|
38
|
+
* - **Literal** — a primitive (`"a"`, `42`, `true`) matched by `===`.
|
|
39
|
+
* - **Object pattern** — a `Partial<T>` of shallow key/value pairs that
|
|
40
|
+
* must all match by strict equality. Only available when `T` is an
|
|
41
|
+
* object type.
|
|
42
|
+
* - **Guard** — a function `(input: T) => boolean`. Type predicates
|
|
43
|
+
* (`(x): x is U`) narrow the handler input via {@link Narrow}.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* type Event = { kind: "click"; x: number } | { kind: "key"; code: string };
|
|
48
|
+
*
|
|
49
|
+
* const p1: Pattern<Event> = { kind: "click" }; // object pattern
|
|
50
|
+
* const p2: Pattern<Event> = (e) => e.kind === "key"; // guard
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
type Pattern<T> = T | ObjectPattern<T> | PatternGuard<T>;
|
|
54
|
+
type ObjectPattern<T> = T extends object ? Partial<T> : never;
|
|
55
|
+
type PatternGuard<T> = (input: T) => boolean;
|
|
56
|
+
type Case<T, R> = {
|
|
57
|
+
readonly test: (input: T) => boolean;
|
|
58
|
+
readonly run: (input: T) => R;
|
|
59
|
+
};
|
|
60
|
+
type NextResult<Locked extends boolean, R, R2> = Locked extends true ? R : R | R2;
|
|
61
|
+
type HandlerReturn<Locked extends boolean, R, R2> = Locked extends true ? R : R2;
|
|
62
|
+
/**
|
|
63
|
+
* Fluent builder for {@link match}. Tracks the handled cases at the type
|
|
64
|
+
* level so `.exhaustive()` only compiles when every union member is covered.
|
|
65
|
+
*
|
|
66
|
+
* The `Locked` phantom flag (set via `.returnType<R>()`) constrains every
|
|
67
|
+
* subsequent handler to return `R`, useful when branch return-type
|
|
68
|
+
* inference would widen to a union narrower than the target slot
|
|
69
|
+
* (e.g. `ReactNode`, an API DTO).
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const describe = match<Event>()
|
|
74
|
+
* .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
|
|
75
|
+
* .with({ kind: "key" }, (e) => `key ${e.code}`)
|
|
76
|
+
* .exhaustive();
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
declare class MatchBuilder<T, R = never, HasInput extends boolean = false, Handled extends readonly unknown[] = [], Locked extends boolean = false> {
|
|
80
|
+
private readonly cases;
|
|
81
|
+
private readonly input?;
|
|
82
|
+
readonly _handled: Handled;
|
|
83
|
+
constructor(cases?: readonly Case<T, unknown>[], input?: T | undefined, _handled?: Handled);
|
|
84
|
+
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
|
+
/** One handler for several patterns (OR). Handler input is the union of narrowed members. */
|
|
86
|
+
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>;
|
|
87
|
+
/** `withOneOf` for exactly two patterns. */
|
|
88
|
+
withEither<const P1 extends Pattern<T>, const P2 extends Pattern<T>, R2>(pattern1: P1, pattern2: P2, handler: (input: NarrowUnion<T, readonly [P1, P2]>) => HandlerReturn<Locked, R, R2>): MatchBuilder<T, NextResult<Locked, R, R2>, HasInput, readonly [...Handled, NarrowUnion<T, readonly [P1, P2]>], Locked>;
|
|
89
|
+
/**
|
|
90
|
+
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
91
|
+
* Useful when branch return-type inference widens to a union narrower than
|
|
92
|
+
* the slot the match feeds into (e.g. `ReactNode`).
|
|
93
|
+
*/
|
|
94
|
+
returnType<R2>(): MatchBuilder<T, R2, HasInput, Handled, true>;
|
|
95
|
+
/** Run on the value passed to {@link match}, or on `input` when curried. */
|
|
96
|
+
run(input?: T): R;
|
|
97
|
+
exhaustive(): ExhaustiveResult<T, Handled, R, HasInput>;
|
|
98
|
+
otherwise(handler: (input: T) => R): HasInput extends true ? R : (input: T) => R;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Result-locked variant of {@link MatchBuilder}. Constructed via
|
|
102
|
+
* `match(...).returnType<R>()`. Now a thin type alias over `MatchBuilder`
|
|
103
|
+
* with the `Locked` phantom flag set — kept for backwards compatibility.
|
|
104
|
+
*/
|
|
105
|
+
type LockedMatchBuilder<T, R, HasInput extends boolean = false, Handled extends readonly unknown[] = []> = MatchBuilder<T, R, HasInput, Handled, true>;
|
|
106
|
+
/**
|
|
107
|
+
* Start a pattern-matching expression. Two call shapes:
|
|
108
|
+
*
|
|
109
|
+
* - `match(value)` — data-first; subsequent `.exhaustive()` or
|
|
110
|
+
* `.otherwise(fn)` runs immediately against `value`.
|
|
111
|
+
* - `match<T>()` — curried; subsequent `.exhaustive()` returns a function
|
|
112
|
+
* `(value: T) => R` for use in `pipe(...)` or as a reusable matcher.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* // Data-first
|
|
117
|
+
* const out = match({ kind: "click", x: 1, y: 2 } as Event)
|
|
118
|
+
* .with({ kind: "click" }, (e) => e.x + e.y)
|
|
119
|
+
* .with({ kind: "key" }, () => -1)
|
|
120
|
+
* .exhaustive();
|
|
121
|
+
*
|
|
122
|
+
* // Curried — build a reusable matcher
|
|
123
|
+
* const describe = match<Event>()
|
|
124
|
+
* .with({ kind: "click" }, (e) => `click @ ${e.x},${e.y}`)
|
|
125
|
+
* .with({ kind: "key" }, (e) => `key ${e.code}`)
|
|
126
|
+
* .exhaustive();
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
declare function match<T>(input: T): MatchBuilder<T, never, true>;
|
|
130
|
+
declare function match<T = never>(): MatchBuilder<T, never, false>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Guard pattern for {@link match}.with.
|
|
134
|
+
*
|
|
135
|
+
* Accepts plain boolean guards (`(x) => x.length > 0`) or TS type-predicate
|
|
136
|
+
* guards (`(x): x is Foo => ...`). For type predicates, the matched handler's
|
|
137
|
+
* input is narrowed to the predicate's target type via {@link Narrow}.
|
|
138
|
+
*/
|
|
139
|
+
declare function when<T, U extends T>(guard: (input: T) => input is U): (input: T) => input is U;
|
|
140
|
+
declare function when<T>(guard: (input: T) => boolean): Pattern<T>;
|
|
141
|
+
|
|
142
|
+
export { type ExhaustMatched, type ExhaustiveResult, type HandledUnion, type IsExhaustive, type LockedMatchBuilder, MatchBuilder, type Narrow, type NarrowUnion, type NonExhaustiveError, type Pattern, type RemainingCases, assertNever, match, when };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/assert.ts
|
|
2
|
+
var assertNever = (value, message = "Unreachable") => {
|
|
3
|
+
throw new Error(`${message}: ${String(value)}`);
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
// src/match.ts
|
|
7
|
+
var isGuard = (pattern) => typeof pattern === "function";
|
|
8
|
+
var isPatternObject = (pattern) => typeof pattern === "object" && pattern !== null;
|
|
9
|
+
var matches = (input, pattern) => {
|
|
10
|
+
if (isGuard(pattern)) {
|
|
11
|
+
return pattern(input);
|
|
12
|
+
}
|
|
13
|
+
if (!isPatternObject(pattern)) {
|
|
14
|
+
return input === pattern;
|
|
15
|
+
}
|
|
16
|
+
if (typeof input !== "object" || input === null) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const record = input;
|
|
20
|
+
const patternRecord = pattern;
|
|
21
|
+
for (const key of Object.keys(patternRecord)) {
|
|
22
|
+
if (record[key] !== patternRecord[key]) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
var NO_MATCH = /* @__PURE__ */ Symbol("@onrails/pattern/no-match");
|
|
29
|
+
var runCases = (input, cases) => {
|
|
30
|
+
for (const c of cases) {
|
|
31
|
+
if (c.test(input)) {
|
|
32
|
+
return c.run(input);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return NO_MATCH;
|
|
36
|
+
};
|
|
37
|
+
var caseForPatterns = (patterns, handler) => ({
|
|
38
|
+
test: (input) => patterns.some((pattern) => matches(input, pattern)),
|
|
39
|
+
run: handler
|
|
40
|
+
});
|
|
41
|
+
var MatchBuilder = class _MatchBuilder {
|
|
42
|
+
constructor(cases = [], input, _handled = []) {
|
|
43
|
+
this.cases = cases;
|
|
44
|
+
this.input = input;
|
|
45
|
+
this._handled = _handled;
|
|
46
|
+
}
|
|
47
|
+
cases;
|
|
48
|
+
input;
|
|
49
|
+
_handled;
|
|
50
|
+
with(pattern, handler) {
|
|
51
|
+
const next = {
|
|
52
|
+
test: (input) => matches(input, pattern),
|
|
53
|
+
run: handler
|
|
54
|
+
};
|
|
55
|
+
return new _MatchBuilder([...this.cases, next], this.input, [
|
|
56
|
+
...this._handled
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
/** One handler for several patterns (OR). Handler input is the union of narrowed members. */
|
|
60
|
+
withOneOf(patterns, handler) {
|
|
61
|
+
const next = caseForPatterns(patterns, handler);
|
|
62
|
+
return new _MatchBuilder([...this.cases, next], this.input, [
|
|
63
|
+
...this._handled
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
/** `withOneOf` for exactly two patterns. */
|
|
67
|
+
withEither(pattern1, pattern2, handler) {
|
|
68
|
+
return this.withOneOf([pattern1, pattern2], handler);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Lock the result type. All subsequent `.with()` handlers must return `R2`.
|
|
72
|
+
* Useful when branch return-type inference widens to a union narrower than
|
|
73
|
+
* the slot the match feeds into (e.g. `ReactNode`).
|
|
74
|
+
*/
|
|
75
|
+
returnType() {
|
|
76
|
+
return new _MatchBuilder(this.cases, this.input, this._handled);
|
|
77
|
+
}
|
|
78
|
+
/** Run on the value passed to {@link match}, or on `input` when curried. */
|
|
79
|
+
run(input) {
|
|
80
|
+
const value = input ?? this.input;
|
|
81
|
+
if (value === void 0) {
|
|
82
|
+
throw new Error("match.run: no input value");
|
|
83
|
+
}
|
|
84
|
+
const out = runCases(value, this.cases);
|
|
85
|
+
if (out === NO_MATCH) {
|
|
86
|
+
throw new Error("Non-exhaustive match: no case matched input");
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
exhaustive() {
|
|
91
|
+
if (this.input !== void 0) {
|
|
92
|
+
return this.run();
|
|
93
|
+
}
|
|
94
|
+
return ((value) => this.run(value));
|
|
95
|
+
}
|
|
96
|
+
otherwise(handler) {
|
|
97
|
+
const runWithFallback = (value) => {
|
|
98
|
+
const out = runCases(value, this.cases);
|
|
99
|
+
return out === NO_MATCH ? handler(value) : out;
|
|
100
|
+
};
|
|
101
|
+
if (this.input !== void 0) {
|
|
102
|
+
return runWithFallback(this.input);
|
|
103
|
+
}
|
|
104
|
+
return runWithFallback;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function match(input) {
|
|
108
|
+
return new MatchBuilder([], input);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/tag.ts
|
|
112
|
+
var matchTag = (value, handlers) => {
|
|
113
|
+
const tag = value._tag;
|
|
114
|
+
const handler = handlers[tag];
|
|
115
|
+
return handler(value);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/when.ts
|
|
119
|
+
function when(guard) {
|
|
120
|
+
return guard;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { MatchBuilder, assertNever, match, matchTag, when };
|
|
124
|
+
//# sourceMappingURL=index.js.map
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
package/dist/tag.cjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/tag.ts
|
|
4
|
+
var matchTag = (value, handlers) => {
|
|
5
|
+
const tag = value._tag;
|
|
6
|
+
const handler = handlers[tag];
|
|
7
|
+
return handler(value);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
exports.matchTag = matchTag;
|
|
11
|
+
//# sourceMappingURL=tag.cjs.map
|
|
12
|
+
//# sourceMappingURL=tag.cjs.map
|
package/dist/tag.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tag.ts"],"names":[],"mappings":";;;AASO,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","file":"tag.cjs","sourcesContent":["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"]}
|
package/dist/tag.d.cts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type Tagged = {
|
|
2
|
+
readonly _tag: string;
|
|
3
|
+
};
|
|
4
|
+
type TagHandlers<T extends Tagged, R> = {
|
|
5
|
+
[K in T["_tag"]]: (value: Extract<T, {
|
|
6
|
+
_tag: K;
|
|
7
|
+
}>) => R;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style unions.
|
|
11
|
+
*/
|
|
12
|
+
declare const matchTag: <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>) => R;
|
|
13
|
+
|
|
14
|
+
export { matchTag };
|
package/dist/tag.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type Tagged = {
|
|
2
|
+
readonly _tag: string;
|
|
3
|
+
};
|
|
4
|
+
type TagHandlers<T extends Tagged, R> = {
|
|
5
|
+
[K in T["_tag"]]: (value: Extract<T, {
|
|
6
|
+
_tag: K;
|
|
7
|
+
}>) => R;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Exhaustive match on `_tag` — for `@onrails/result` / `@onrails/maybe` style unions.
|
|
11
|
+
*/
|
|
12
|
+
declare const matchTag: <T extends Tagged, R>(value: T, handlers: TagHandlers<T, R>) => R;
|
|
13
|
+
|
|
14
|
+
export { matchTag };
|
package/dist/tag.js
ADDED
package/dist/tag.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tag.ts"],"names":[],"mappings":";AASO,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","file":"tag.js","sourcesContent":["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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onrails/pattern",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight exhaustive matching for owned tagged unions — ts-pattern-shaped DX, tree-shakeable",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Alan R. Soares",
|
|
7
|
+
"homepage": "https://alanrsoares.github.io/onrails/modules/_onrails_pattern.html",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/alanrsoares/onrails.git",
|
|
11
|
+
"directory": "packages/pattern"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/alanrsoares/onrails/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pattern-matching",
|
|
18
|
+
"exhaustive",
|
|
19
|
+
"tagged-union",
|
|
20
|
+
"discriminated-union",
|
|
21
|
+
"typescript",
|
|
22
|
+
"bun",
|
|
23
|
+
"functional"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18",
|
|
27
|
+
"bun": ">=1.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"provenance": true
|
|
32
|
+
},
|
|
33
|
+
"type": "module",
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"main": "./dist/index.cjs",
|
|
36
|
+
"module": "./dist/index.js",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"DESIGN.md",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"import": "./dist/index.js",
|
|
48
|
+
"require": "./dist/index.cjs"
|
|
49
|
+
},
|
|
50
|
+
"./tag": {
|
|
51
|
+
"types": "./dist/tag.d.ts",
|
|
52
|
+
"import": "./dist/tag.js",
|
|
53
|
+
"require": "./dist/tag.cjs"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"test": "bun test",
|
|
59
|
+
"check": "bun typecheck && bun test",
|
|
60
|
+
"build": "tsup",
|
|
61
|
+
"clean": "rm -rf dist",
|
|
62
|
+
"prepublishOnly": "bun run build"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@onrails/result": "0.0.0",
|
|
66
|
+
"ts-expect": "^1.3.0",
|
|
67
|
+
"tsup": "^8.5.1"
|
|
68
|
+
}
|
|
69
|
+
}
|