@semyonf/kamchazky 0.2.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/LICENSE +21 -0
- package/README.md +352 -0
- package/dist/index.cjs +482 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +340 -0
- package/dist/index.d.ts +340 -0
- package/dist/index.js +447 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Semyon Fomin
|
|
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,352 @@
|
|
|
1
|
+
# kamchazky
|
|
2
|
+
|
|
3
|
+
Type-safe `Result` and `Maybe` monads for TypeScript. Explicit error handling and optional values with full type inference — no exceptions required.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install @semyonf/kamchazky
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
`Result<T, E>` is a discriminated union. Check `result.ok` once and TypeScript knows exactly what you have — no `.unwrap()`, no casting, no runtime surprises:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
declare const result: Result<User, ApiError>;
|
|
17
|
+
|
|
18
|
+
if (result.ok) {
|
|
19
|
+
result.value; // User
|
|
20
|
+
} else {
|
|
21
|
+
result.error; // ApiError
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare const maybe: Maybe<User>;
|
|
25
|
+
|
|
26
|
+
if (maybe.some) {
|
|
27
|
+
maybe.value; // User
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Works in ternaries, early returns, switches — anywhere TypeScript does control-flow analysis.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Most Result libraries (including oxide.ts) expose `.ok()` as a *method*, which returns a plain `boolean`. TypeScript sees no connection between that boolean and the original type, so you still have to call `.unwrap()` and hope it doesn't throw, which kinda defeats the purpose. This library uses a discriminant field so the compiler does the work for you.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Result
|
|
38
|
+
|
|
39
|
+
### Types
|
|
40
|
+
|
|
41
|
+
#### `Result<T, E extends Error = Error>`
|
|
42
|
+
|
|
43
|
+
Union of `OkResult<T> | ErrResult<E>`.
|
|
44
|
+
|
|
45
|
+
#### `OkResult<T>`
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
type OkResult<T> = {
|
|
49
|
+
readonly ok: true;
|
|
50
|
+
readonly value: T;
|
|
51
|
+
map<U>(fn: (value: T) => U): OkResult<U>;
|
|
52
|
+
flatMap<U, F extends Error = never>(fn: (value: T) => Result<U, F>): Result<U, F>;
|
|
53
|
+
mapError<F extends Error>(fn: (error: never) => F): OkResult<T>;
|
|
54
|
+
inspect(fn: (value: T) => void): OkResult<T>;
|
|
55
|
+
inspectError(fn: (error: never) => void): OkResult<T>;
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### `ErrResult<E extends Error>`
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
type ErrResult<E extends Error = Error> = {
|
|
63
|
+
readonly ok: false;
|
|
64
|
+
readonly error: E;
|
|
65
|
+
map<U>(fn: (value: never) => U): ErrResult<E>;
|
|
66
|
+
flatMap<U, F extends Error = never>(fn: (value: never) => Result<U, F>): ErrResult<E>;
|
|
67
|
+
mapError<F extends Error>(fn: (error: E) => F): ErrResult<F>;
|
|
68
|
+
inspect(fn: (value: never) => void): ErrResult<E>;
|
|
69
|
+
inspectError(fn: (error: E) => void): ErrResult<E>;
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### API
|
|
74
|
+
|
|
75
|
+
All functions are available on the `Result` namespace and the most common ones (`ok`, `err`, `isOk`, `isErr`, `normalizeError`) are also exported at the top level.
|
|
76
|
+
|
|
77
|
+
#### Creating results
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// From values
|
|
81
|
+
Result.ok(42); // OkResult<number>
|
|
82
|
+
Result.err(new Error("fail")); // ErrResult<Error>
|
|
83
|
+
|
|
84
|
+
// From nullable values
|
|
85
|
+
Result.fromNullable(
|
|
86
|
+
await findUser(id),
|
|
87
|
+
() => new NotFoundError(`User ${id} not found`),
|
|
88
|
+
); // Result<User, NotFoundError>
|
|
89
|
+
|
|
90
|
+
// From predicates (supports type guards)
|
|
91
|
+
Result.fromPredicate(
|
|
92
|
+
value,
|
|
93
|
+
(v): v is string => typeof v === "string",
|
|
94
|
+
() => new TypeError("expected string"),
|
|
95
|
+
); // Result<string, TypeError>
|
|
96
|
+
|
|
97
|
+
// From functions that may throw
|
|
98
|
+
Result.tryCatch(() => JSON.parse(input));
|
|
99
|
+
Result.tryCatchAsync(() => fetch("/api").then(r => r.json()));
|
|
100
|
+
|
|
101
|
+
// From promises that may reject
|
|
102
|
+
Result.fromPromise(fetch("/api"));
|
|
103
|
+
Result.fromPromise(
|
|
104
|
+
fetch("/api"),
|
|
105
|
+
(e) => new NetworkError(String(e)),
|
|
106
|
+
); // Promise<Result<Response, NetworkError>>
|
|
107
|
+
|
|
108
|
+
// With custom error mapping
|
|
109
|
+
Result.tryCatch(
|
|
110
|
+
() => JSON.parse(input),
|
|
111
|
+
(e) => new ParseError(String(e)),
|
|
112
|
+
); // Result<unknown, ParseError>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Transforming
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Transform the value
|
|
119
|
+
Result.map(result, (value) => value * 2);
|
|
120
|
+
|
|
121
|
+
// Transform the error
|
|
122
|
+
Result.mapError(result, (e) => new AppError(e.message));
|
|
123
|
+
|
|
124
|
+
// Chain Result-returning operations
|
|
125
|
+
Result.flatMap(result, (value) => validate(value));
|
|
126
|
+
|
|
127
|
+
// Recover from errors
|
|
128
|
+
Result.orElse(result, (error) => ok(defaultValue));
|
|
129
|
+
|
|
130
|
+
// Flatten nested Results (inner and outer error types can differ)
|
|
131
|
+
Result.flatten(ok(ok(42))); // ok(42)
|
|
132
|
+
|
|
133
|
+
// Observe values without transforming (useful for logging)
|
|
134
|
+
Result.inspect(result, (value) => console.log("got", value));
|
|
135
|
+
Result.inspectError(result, (error) => console.error(error));
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
All transformations are also available as instance methods for chaining:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
ok(10)
|
|
142
|
+
.inspect((x) => console.log("start:", x))
|
|
143
|
+
.map((x) => x + 5)
|
|
144
|
+
.flatMap((x) => x > 10 ? ok(x) : err(new Error("too small")))
|
|
145
|
+
.mapError((e) => new AppError(e.message))
|
|
146
|
+
.inspectError((e) => console.error(e));
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### Extracting values
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
Result.unwrap(result); // returns value or throws error
|
|
153
|
+
Result.unwrapOr(result, fallback); // returns value or fallback (same type)
|
|
154
|
+
Result.expect(result, "msg"); // returns value or throws with message
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`unwrapOr` requires the fallback to be the same type `T` as the success value. Use `match` when you need a different return type.
|
|
158
|
+
|
|
159
|
+
#### Pattern matching
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
Result.match(result, {
|
|
163
|
+
ok: (value) => `Success: ${value}`,
|
|
164
|
+
err: (error) => `Error: ${error.message}`,
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### Combining results
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// Combine multiple results — fail-fast, returns first error
|
|
172
|
+
const r = Result.all(fetchUser(id), fetchPosts(id));
|
|
173
|
+
// Result<[User, Post[]], Error>
|
|
174
|
+
|
|
175
|
+
// Async variant — resolves all promises concurrently
|
|
176
|
+
const r = await Result.allAsync(fetchUserAsync(id), fetchPostsAsync(id));
|
|
177
|
+
// Result<[User, Post[]], Error>
|
|
178
|
+
|
|
179
|
+
// Collect all errors instead of failing fast
|
|
180
|
+
const r = Result.collect(
|
|
181
|
+
validateName(input.name),
|
|
182
|
+
validateEmail(input.email),
|
|
183
|
+
validateAge(input.age),
|
|
184
|
+
); // Result<[Name, Email, Age], AggregateError>
|
|
185
|
+
// On failure: r.error.errors contains all individual errors
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`all` and `collect` use exact tuple inference — pass results as individual arguments. Spreading an array (`...arr`) loses the tuple types and degrades values to `unknown[]`.
|
|
189
|
+
|
|
190
|
+
#### Error normalization
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
Result.normalizeError(new TypeError("t")); // returns the TypeError as-is
|
|
194
|
+
Result.normalizeError("oops"); // new Error("oops")
|
|
195
|
+
Result.normalizeError(42); // new Error("42")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Maybe
|
|
201
|
+
|
|
202
|
+
### Types
|
|
203
|
+
|
|
204
|
+
#### `Maybe<T>`
|
|
205
|
+
|
|
206
|
+
Union of `Some<T> | None`.
|
|
207
|
+
|
|
208
|
+
#### `Some<T>`
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
type Some<T> = {
|
|
212
|
+
readonly some: true;
|
|
213
|
+
readonly value: T;
|
|
214
|
+
map<U>(fn: (value: T) => U): Some<U>;
|
|
215
|
+
flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U>;
|
|
216
|
+
filter(predicate: (value: T) => boolean): Maybe<T>;
|
|
217
|
+
inspect(fn: (value: T) => void): Some<T>;
|
|
218
|
+
};
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### `None`
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
type None = {
|
|
225
|
+
readonly some: false;
|
|
226
|
+
map<U>(fn: (value: never) => U): None;
|
|
227
|
+
flatMap<U>(fn: (value: never) => Maybe<U>): None;
|
|
228
|
+
filter(predicate: (value: never) => boolean): None;
|
|
229
|
+
inspect(fn: (value: never) => void): None;
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### API
|
|
234
|
+
|
|
235
|
+
All functions are available on the `Maybe` namespace and the most common ones (`some`, `none`, `isSome`, `isNone`) are also exported at the top level.
|
|
236
|
+
|
|
237
|
+
#### Creating maybes
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// From values
|
|
241
|
+
Maybe.some(42); // Some<number>
|
|
242
|
+
Maybe.none(); // None
|
|
243
|
+
|
|
244
|
+
// From nullable values (no error factory needed, unlike Result)
|
|
245
|
+
Maybe.fromNullable(document.getElementById("app")); // Maybe<HTMLElement>
|
|
246
|
+
|
|
247
|
+
// From predicates (supports type guards)
|
|
248
|
+
Maybe.fromPredicate(
|
|
249
|
+
value,
|
|
250
|
+
(v): v is string => typeof v === "string",
|
|
251
|
+
); // Maybe<string>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### Transforming
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Transform the value
|
|
258
|
+
Maybe.map(maybe, (value) => value * 2);
|
|
259
|
+
|
|
260
|
+
// Chain Maybe-returning operations
|
|
261
|
+
Maybe.flatMap(maybe, (value) => findById(value));
|
|
262
|
+
|
|
263
|
+
// Flatten nested Maybes
|
|
264
|
+
Maybe.flatten(some(some(42))); // some(42)
|
|
265
|
+
|
|
266
|
+
// Filter — keep Some only if predicate passes (supports type guards)
|
|
267
|
+
Maybe.filter(maybe, (x) => x > 0);
|
|
268
|
+
|
|
269
|
+
// Observe values without transforming
|
|
270
|
+
Maybe.inspect(maybe, (value) => console.log("got", value));
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
All transformations are also available as instance methods for chaining:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
some(10)
|
|
277
|
+
.inspect((x) => console.log("start:", x))
|
|
278
|
+
.map((x) => x + 5)
|
|
279
|
+
.flatMap((x) => x > 10 ? some(x) : none())
|
|
280
|
+
.filter((x) => x < 100);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### Extracting values
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
Maybe.unwrap(maybe); // returns value or throws
|
|
287
|
+
Maybe.unwrapOr(maybe, fallback); // returns value or fallback (same type)
|
|
288
|
+
Maybe.expect(maybe, "msg"); // returns value or throws with message
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### Pattern matching
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
Maybe.match(maybe, {
|
|
295
|
+
some: (value) => `Found: ${value}`,
|
|
296
|
+
none: () => "Not found",
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### Combining maybes
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// Combine multiple maybes — returns None if any is None
|
|
304
|
+
const m = Maybe.all(findUser(id), findSettings(id));
|
|
305
|
+
// Maybe<[User, Settings]>
|
|
306
|
+
|
|
307
|
+
// Return the first Some found
|
|
308
|
+
const m = Maybe.firstSome(fromCache(id), fromDb(id), defaultValue);
|
|
309
|
+
// Maybe<User>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Interop
|
|
315
|
+
|
|
316
|
+
Convert between `Result` and `Maybe`:
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// Result → Maybe (discards the error)
|
|
320
|
+
Result.toMaybe(ok(42)); // some(42)
|
|
321
|
+
Result.toMaybe(err(new Error())); // none()
|
|
322
|
+
|
|
323
|
+
// Maybe → Result (requires an error for the None case)
|
|
324
|
+
Result.fromMaybe(some(42), new Error("missing")); // ok(42)
|
|
325
|
+
Result.fromMaybe(none(), new Error("missing")); // err(Error("missing"))
|
|
326
|
+
|
|
327
|
+
// Same operations from the Maybe side
|
|
328
|
+
Maybe.fromResult(ok(42)); // some(42)
|
|
329
|
+
Maybe.toResult(some(42), new Error("missing")); // ok(42)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Design Decisions
|
|
335
|
+
|
|
336
|
+
**Discriminant fields, not methods** — `result.ok` and `maybe.some` are boolean fields, not methods. TypeScript's control-flow analysis works directly with discriminant fields, giving you automatic narrowing without `.unwrap()`.
|
|
337
|
+
|
|
338
|
+
**Error type must extend `Error`** — prevents using strings or arbitrary values as errors while allowing custom Error subclasses.
|
|
339
|
+
|
|
340
|
+
**Singleton `None`** — `none()` always returns the same frozen object. Safe to compare with `===`.
|
|
341
|
+
|
|
342
|
+
**Fail-fast `all()`** — returns the first error/None rather than collecting all errors. Use `Result.collect()` when you need all errors (e.g., validation).
|
|
343
|
+
|
|
344
|
+
**`collect()` uses `AggregateError`** — a standard ES2021 `Error` subclass with an `.errors` array, satisfying the `E extends Error` constraint without custom types.
|
|
345
|
+
|
|
346
|
+
**`flatten()` supports different error types** — inner and outer `Result`s can have different error types; the result unions them (`E | F`).
|
|
347
|
+
|
|
348
|
+
**No error type on `OkResult` / no value on `None`** — keeps signatures clean and prevents ghost types from polluting the wrong branch.
|
|
349
|
+
|
|
350
|
+
## License
|
|
351
|
+
|
|
352
|
+
MIT
|