dismatch 0.0.2

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.
Files changed (3) hide show
  1. package/README.md +269 -0
  2. package/lib/index.js +11 -0
  3. package/package.json +33 -0
package/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # dismatch
2
+
3
+ Type-safe pattern matching for TypeScript discriminated unions. Zero dependencies. Full type inference. Exhaustive checking at compile time.
4
+
5
+ Stop writing `switch` statements. Start matching.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install dismatch
11
+ ```
12
+
13
+ ## The Problem
14
+
15
+ TypeScript discriminated unions are powerful, but working with them is tedious:
16
+
17
+ ```ts
18
+ type Shape =
19
+ | { type: 'circle'; radius: number }
20
+ | { type: 'rectangle'; width: number; height: number }
21
+ | { type: 'triangle'; base: number; height: number };
22
+
23
+ // This gets old fast
24
+ function area(shape: Shape): number {
25
+ switch (shape.type) {
26
+ case 'circle':
27
+ return Math.PI * shape.radius ** 2;
28
+ case 'rectangle':
29
+ return shape.width * shape.height;
30
+ case 'triangle':
31
+ return (shape.base * shape.height) / 2;
32
+ }
33
+ }
34
+ ```
35
+
36
+ Add a new variant to `Shape` and the compiler won't tell you about the 14 switch statements you forgot to update.
37
+
38
+ ## The Solution
39
+
40
+ ```ts
41
+ import { match } from 'dismatch';
42
+
43
+ const area = match(shape)({
44
+ circle: ({ radius }) => Math.PI * radius ** 2,
45
+ rectangle: ({ width, height }) => width * height,
46
+ triangle: ({ base, height }) => (base * height) / 2,
47
+ });
48
+ ```
49
+
50
+ Add a new variant. TypeScript screams at every `match` call that's missing a handler. That's the point.
51
+
52
+ ## API
53
+
54
+ ### `match(union)(handlers)` — Exhaustive Pattern Matching
55
+
56
+ Every variant must have a handler. Miss one and TypeScript won't compile.
57
+
58
+ ```ts
59
+ type Result =
60
+ | { type: 'ok'; data: string }
61
+ | { type: 'error'; message: string }
62
+ | { type: 'loading' };
63
+
64
+ const response: Result = { type: 'ok', data: 'hello' };
65
+
66
+ const message = match(response)({
67
+ ok: ({ data }) => `Got: ${data}`,
68
+ error: ({ message }) => `Failed: ${message}`,
69
+ loading: () => 'Loading...',
70
+ });
71
+ // message: "Got: hello"
72
+ ```
73
+
74
+ The return type is inferred from your handlers. Return numbers, strings, objects, whatever you want — it just works.
75
+
76
+ ### `matchWithDefault(union)(handlers)` — Match With Fallback
77
+
78
+ Handle some variants explicitly. Catch the rest with `Default`.
79
+
80
+ ```ts
81
+ const label = matchWithDefault(response)({
82
+ error: ({ message }) => `Error: ${message}`,
83
+ Default: () => 'Everything is fine',
84
+ });
85
+ ```
86
+
87
+ Useful when you only care about specific variants and want a catch-all for the rest.
88
+
89
+ ### `map(union)(handlers)` — Partial Transformation
90
+
91
+ Transform specific variants. The rest pass through unchanged.
92
+
93
+ ```ts
94
+ const doubled = map(shape)({
95
+ circle: ({ type, radius }) => ({ type, radius: radius * 2 }),
96
+ });
97
+ // If shape is a circle: radius is doubled
98
+ // If shape is anything else: returned as-is, untouched
99
+ ```
100
+
101
+ Think of it as "update where type matches". Handlers you don't provide default to identity.
102
+
103
+ ### `mapAll(union)(handlers)` — Full Transformation
104
+
105
+ Like `map`, but every variant must have a handler. No freebies.
106
+
107
+ ```ts
108
+ const normalized = mapAll(shape)({
109
+ circle: ({ type, radius }) => ({ type, radius: Math.abs(radius) }),
110
+ rectangle: ({ type, width, height }) => ({ type, width: Math.abs(width), height: Math.abs(height) }),
111
+ triangle: ({ type, base, height }) => ({ type, base: Math.abs(base), height: Math.abs(height) }),
112
+ });
113
+ ```
114
+
115
+ ### `is(union, type)` — Type Guard
116
+
117
+ Narrows a union to a specific variant. Works in `if` statements, `filter` calls, anywhere TypeScript expects a type predicate.
118
+
119
+ ```ts
120
+ import { is } from 'dismatch';
121
+
122
+ if (is(shape, 'circle')) {
123
+ // TypeScript knows: shape is { type: 'circle'; radius: number }
124
+ console.log(shape.radius);
125
+ }
126
+
127
+ // Works great with array filtering
128
+ const circles = shapes.filter((s) => is(s, 'circle'));
129
+ // circles: { type: 'circle'; radius: number }[]
130
+ ```
131
+
132
+ ### `isUnion(value)` — Runtime Validation
133
+
134
+ Checks whether a value is a valid discriminated union (a non-null object with a string `type` property). Useful at system boundaries — API responses, form data, anything you can't trust at compile time.
135
+
136
+ ```ts
137
+ import { isUnion } from 'dismatch';
138
+
139
+ isUnion({ type: 'circle', radius: 5 }); // true
140
+ isUnion({ name: 'no type field' }); // false
141
+ isUnion(null); // false
142
+ isUnion('string'); // false
143
+ ```
144
+
145
+ ## Defining Your Unions
146
+
147
+ Use plain TypeScript types. Any object with a `type` string discriminant works:
148
+
149
+ ```ts
150
+ type ApiResponse =
151
+ | { type: 'success'; data: User[] }
152
+ | { type: 'error'; code: number; message: string }
153
+ | { type: 'unauthorized' };
154
+ ```
155
+
156
+ Or use the `Model` helper type for cleaner definitions:
157
+
158
+ ```ts
159
+ import type { Model } from 'dismatch';
160
+
161
+ type Success = Model<'success', { data: User[] }>;
162
+ type ApiError = Model<'error', { code: number; message: string }>;
163
+ type Unauthorized = Model<'unauthorized', {}>;
164
+
165
+ type ApiResponse = Success | ApiError | Unauthorized;
166
+ ```
167
+
168
+ ## Curried by Design
169
+
170
+ Every function returns a reusable matcher. Bind the data once, apply different handlers later:
171
+
172
+ ```ts
173
+ const handleResponse = match(response);
174
+
175
+ // Use the same bound value with different matchers
176
+ const statusCode = handleResponse({
177
+ ok: () => 200,
178
+ error: ({ code }) => code,
179
+ loading: () => 0,
180
+ });
181
+
182
+ const isHealthy = handleResponse({
183
+ ok: () => true,
184
+ error: () => false,
185
+ loading: () => false,
186
+ });
187
+ ```
188
+
189
+ ## Real-World Examples
190
+
191
+ ### State Machines
192
+
193
+ ```ts
194
+ type AuthState =
195
+ | { type: 'logged_out' }
196
+ | { type: 'logging_in'; email: string }
197
+ | { type: 'logged_in'; user: User; token: string }
198
+ | { type: 'error'; reason: string };
199
+
200
+ function renderAuth(state: AuthState) {
201
+ return match(state)({
202
+ logged_out: () => renderLoginForm(),
203
+ logging_in: ({ email }) => renderSpinner(`Signing in ${email}...`),
204
+ logged_in: ({ user }) => renderDashboard(user),
205
+ error: ({ reason }) => renderError(reason),
206
+ });
207
+ }
208
+ ```
209
+
210
+ ### Reducer Actions
211
+
212
+ ```ts
213
+ type Action =
214
+ | { type: 'increment'; amount: number }
215
+ | { type: 'decrement'; amount: number }
216
+ | { type: 'reset' };
217
+
218
+ function reducer(state: number, action: Action): number {
219
+ return match(action)({
220
+ increment: ({ amount }) => state + amount,
221
+ decrement: ({ amount }) => state - amount,
222
+ reset: () => 0,
223
+ });
224
+ }
225
+ ```
226
+
227
+ ### API Response Handling
228
+
229
+ ```ts
230
+ type FetchResult<T> =
231
+ | { type: 'pending' }
232
+ | { type: 'fulfilled'; data: T }
233
+ | { type: 'rejected'; error: Error };
234
+
235
+ function unwrapOr<T>(result: FetchResult<T>, fallback: T): T {
236
+ return matchWithDefault(result)({
237
+ fulfilled: ({ data }) => data,
238
+ Default: () => fallback,
239
+ });
240
+ }
241
+ ```
242
+
243
+ ### Selective Updates
244
+
245
+ ```ts
246
+ type Notification =
247
+ | { type: 'email'; subject: string; read: boolean }
248
+ | { type: 'sms'; body: string; read: boolean }
249
+ | { type: 'push'; title: string; read: boolean };
250
+
251
+ // Mark only emails as read, leave everything else alone
252
+ const markEmailsRead = (n: Notification) =>
253
+ map(n)({
254
+ email: ({ type, subject }) => ({ type, subject, read: true }),
255
+ });
256
+ ```
257
+
258
+ ## Scripts
259
+
260
+ ```bash
261
+ npm run build # Compile TypeScript
262
+ npm test # Run tests
263
+ npm run test:watch # Run tests in watch mode
264
+ npm run ts:ci # Type check (no emit)
265
+ ```
266
+
267
+ ## License
268
+
269
+ ISC
package/lib/index.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isUnion = exports.is = exports.mapAll = exports.map = exports.matchWithDefault = exports.match = void 0;
4
+ var unions_1 = require("./unions");
5
+ Object.defineProperty(exports, "match", { enumerable: true, get: function () { return unions_1.match; } });
6
+ Object.defineProperty(exports, "matchWithDefault", { enumerable: true, get: function () { return unions_1.matchWithDefault; } });
7
+ Object.defineProperty(exports, "map", { enumerable: true, get: function () { return unions_1.map; } });
8
+ Object.defineProperty(exports, "mapAll", { enumerable: true, get: function () { return unions_1.mapAll; } });
9
+ var module_1 = require("./module");
10
+ Object.defineProperty(exports, "is", { enumerable: true, get: function () { return module_1.is; } });
11
+ Object.defineProperty(exports, "isUnion", { enumerable: true, get: function () { return module_1.isUnion; } });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "dismatch",
3
+ "description": "A lightweight discriminated unions library for TypeScript",
4
+ "version": "0.0.2",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "keywords": [
8
+ "typescript",
9
+ "discriminated-unions",
10
+ "pattern-matching"
11
+ ],
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "author": "Amir Gorji",
16
+ "license": "MIT",
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "clean": "rm -rf lib",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "ts:ci": "tsc --noEmit",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/amir-gorji/discriminated-unions.git"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5.9.3",
31
+ "vitest": "^4.0.18"
32
+ }
33
+ }