nalloc 0.4.0 → 0.5.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/README.md +300 -180
- package/build/codemod-cli.cjs +153 -0
- package/build/codemod-cli.cjs.map +1 -0
- package/build/codemod-cli.d.ts +2 -0
- package/build/codemod-cli.js +103 -0
- package/build/codemod-cli.js.map +1 -0
- package/build/codemod.cjs +652 -0
- package/build/codemod.cjs.map +1 -0
- package/build/codemod.d.ts +29 -0
- package/build/codemod.js +634 -0
- package/build/codemod.js.map +1 -0
- package/build/eslint.cjs +221 -0
- package/build/eslint.cjs.map +1 -0
- package/build/eslint.d.ts +36 -0
- package/build/eslint.js +198 -0
- package/build/eslint.js.map +1 -0
- package/build/http.cjs +31 -0
- package/build/http.cjs.map +1 -0
- package/build/http.d.ts +31 -0
- package/build/http.js +13 -0
- package/build/http.js.map +1 -0
- package/build/result.cjs +0 -11
- package/build/result.cjs.map +1 -1
- package/build/result.d.ts +0 -32
- package/build/result.js +0 -8
- package/build/result.js.map +1 -1
- package/build/safe.cjs +4 -0
- package/build/safe.cjs.map +1 -1
- package/build/safe.d.ts +1 -0
- package/build/safe.js +1 -0
- package/build/safe.js.map +1 -1
- package/build/schema.cjs +32 -0
- package/build/schema.cjs.map +1 -0
- package/build/schema.d.ts +44 -0
- package/build/schema.js +14 -0
- package/build/schema.js.map +1 -0
- package/package.json +54 -5
- package/src/__tests__/codemod.ts +211 -0
- package/src/__tests__/eslint.ts +99 -0
- package/src/__tests__/fixtures/tsconfig.json +10 -0
- package/src/__tests__/http.ts +64 -0
- package/src/__tests__/result.ts +74 -125
- package/src/__tests__/schema.ts +58 -0
- package/src/codemod-cli.ts +108 -0
- package/src/codemod.ts +623 -0
- package/src/eslint.ts +145 -0
- package/src/http.ts +42 -0
- package/src/result.ts +0 -37
- package/src/safe.ts +1 -0
- package/src/schema.ts +52 -0
package/README.md
CHANGED
|
@@ -25,13 +25,13 @@ nalloc: Ok(data) --> data // zero allocation
|
|
|
25
25
|
|
|
26
26
|
## Benchmarks
|
|
27
27
|
|
|
28
|
-
| Operation
|
|
29
|
-
|
|
30
|
-
| `ok()`
|
|
31
|
-
| `err()`
|
|
32
|
-
| `some()`
|
|
33
|
-
| `Result.map` | **74M ops/s** | 53M ops/s
|
|
34
|
-
| `Option.map` | **71M ops/s** | -
|
|
28
|
+
| Operation | nalloc | neverthrow | ts-results | oxide.ts |
|
|
29
|
+
| ------------ | ------------- | ---------- | ---------- | --------- |
|
|
30
|
+
| `ok()` | **91M ops/s** | 75M ops/s | 56M ops/s | 40M ops/s |
|
|
31
|
+
| `err()` | **55M ops/s** | 52M ops/s | 0.1M ops/s | 47M ops/s |
|
|
32
|
+
| `some()` | **85M ops/s** | - | 40M ops/s | 50M ops/s |
|
|
33
|
+
| `Result.map` | **74M ops/s** | 53M ops/s | 47M ops/s | 64M ops/s |
|
|
34
|
+
| `Option.map` | **71M ops/s** | - | 52M ops/s | 41M ops/s |
|
|
35
35
|
|
|
36
36
|
Run `pnpm bench` to reproduce.
|
|
37
37
|
|
|
@@ -51,15 +51,18 @@ const port = Option.fromNullable(process.env.PORT);
|
|
|
51
51
|
const portNum = Option.unwrapOr(port, '3000');
|
|
52
52
|
|
|
53
53
|
// Result: Ok is the value itself, Err wraps the error
|
|
54
|
-
const config = Result.tryCatch(
|
|
54
|
+
const config = Result.tryCatch(
|
|
55
|
+
() => JSON.parse(raw),
|
|
56
|
+
() => 'invalid json',
|
|
57
|
+
);
|
|
55
58
|
const data = Result.match(
|
|
56
59
|
config,
|
|
57
|
-
cfg => cfg,
|
|
58
|
-
error => ({ fallback: true })
|
|
60
|
+
(cfg) => cfg,
|
|
61
|
+
(error) => ({ fallback: true }),
|
|
59
62
|
);
|
|
60
63
|
|
|
61
64
|
// gen: Rust-like ? operator with type-safe errors
|
|
62
|
-
const result = gen(function*($) {
|
|
65
|
+
const result = gen(function* ($) {
|
|
63
66
|
const a = yield* $(parseNumber('10'));
|
|
64
67
|
const b = yield* $(parseNumber('5'));
|
|
65
68
|
return a + b;
|
|
@@ -68,20 +71,20 @@ const result = gen(function*($) {
|
|
|
68
71
|
// pipe: left-to-right composition
|
|
69
72
|
const userId = pipe(
|
|
70
73
|
Result.tryCatch(() => JSON.parse(raw)),
|
|
71
|
-
r => Result.map(r, data => data.userId),
|
|
72
|
-
r => Result.unwrapOr(r, 0),
|
|
74
|
+
(r) => Result.map(r, (data) => data.userId),
|
|
75
|
+
(r) => Result.unwrapOr(r, 0),
|
|
73
76
|
);
|
|
74
77
|
```
|
|
75
78
|
|
|
76
79
|
## How it works
|
|
77
80
|
|
|
78
|
-
| Type
|
|
79
|
-
|
|
80
|
-
| `Option<T>` | `T \| null \| undefined`
|
|
81
|
-
| `Some<T>`
|
|
82
|
-
| `None`
|
|
83
|
-
| `Ok<T>`
|
|
84
|
-
| `Err<E>`
|
|
81
|
+
| Type | Representation | Allocation |
|
|
82
|
+
| ----------- | ------------------------------ | ---------- |
|
|
83
|
+
| `Option<T>` | `T \| null \| undefined` | Zero |
|
|
84
|
+
| `Some<T>` | The value itself | Zero |
|
|
85
|
+
| `None` | `null` or `undefined` | Zero |
|
|
86
|
+
| `Ok<T>` | The value itself (branded) | Zero |
|
|
87
|
+
| `Err<E>` | Minimal wrapper `{ error: E }` | One object |
|
|
85
88
|
|
|
86
89
|
This means zero GC pressure on the happy path - your success values stay as plain values.
|
|
87
90
|
|
|
@@ -93,12 +96,12 @@ This means zero GC pressure on the happy path - your success values stay as plai
|
|
|
93
96
|
// neverthrow
|
|
94
97
|
import { ok, err, Result } from 'neverthrow';
|
|
95
98
|
const result: Result<number, string> = ok(42);
|
|
96
|
-
result.map(x => x * 2);
|
|
99
|
+
result.map((x) => x * 2);
|
|
97
100
|
|
|
98
101
|
// nalloc - same concepts, faster execution
|
|
99
102
|
import { Result, ok, err } from 'nalloc';
|
|
100
|
-
const result = ok(42);
|
|
101
|
-
Result.map(result, x => x * 2);
|
|
103
|
+
const result = ok(42); // zero allocation
|
|
104
|
+
Result.map(result, (x) => x * 2); // function-based API
|
|
102
105
|
```
|
|
103
106
|
|
|
104
107
|
### From fp-ts
|
|
@@ -107,11 +110,14 @@ Result.map(result, x => x * 2); // function-based API
|
|
|
107
110
|
// fp-ts
|
|
108
111
|
import { pipe } from 'fp-ts/function';
|
|
109
112
|
import * as O from 'fp-ts/Option';
|
|
110
|
-
pipe(
|
|
113
|
+
pipe(
|
|
114
|
+
O.some(42),
|
|
115
|
+
O.map((x) => x * 2),
|
|
116
|
+
);
|
|
111
117
|
|
|
112
118
|
// nalloc - simpler, faster
|
|
113
119
|
import { Option, pipe } from 'nalloc';
|
|
114
|
-
pipe(42, v => Option.map(v, x => x * 2)); // built-in pipe, value IS the Option
|
|
120
|
+
pipe(42, (v) => Option.map(v, (x) => x * 2)); // built-in pipe, value IS the Option
|
|
115
121
|
```
|
|
116
122
|
|
|
117
123
|
### From oxide.ts
|
|
@@ -120,15 +126,15 @@ pipe(42, v => Option.map(v, x => x * 2)); // built-in pipe, value IS the Option
|
|
|
120
126
|
// oxide.ts
|
|
121
127
|
import { Some, None, Ok, Err } from 'oxide.ts';
|
|
122
128
|
const opt = Some(42);
|
|
123
|
-
opt.map(x => x * 2);
|
|
129
|
+
opt.map((x) => x * 2);
|
|
124
130
|
const result = Ok(42);
|
|
125
|
-
result.mapErr(e => new Error(e));
|
|
131
|
+
result.mapErr((e) => new Error(e));
|
|
126
132
|
|
|
127
133
|
// nalloc - no wrapper objects on the happy path
|
|
128
134
|
import { Option, Result, ok } from 'nalloc';
|
|
129
|
-
Option.map(42, x => x * 2);
|
|
130
|
-
const result = ok(42);
|
|
131
|
-
Result.mapErr(result, e => new Error(e));
|
|
135
|
+
Option.map(42, (x) => x * 2); // 42 is the Option itself
|
|
136
|
+
const result = ok(42); // zero allocation
|
|
137
|
+
Result.mapErr(result, (e) => new Error(e));
|
|
132
138
|
```
|
|
133
139
|
|
|
134
140
|
### From ts-results
|
|
@@ -137,15 +143,15 @@ Result.mapErr(result, e => new Error(e));
|
|
|
137
143
|
// ts-results
|
|
138
144
|
import { Ok, Err, Some, None } from 'ts-results';
|
|
139
145
|
const result = new Ok(42);
|
|
140
|
-
result.map(x => x * 2);
|
|
146
|
+
result.map((x) => x * 2);
|
|
141
147
|
const opt = Some(42);
|
|
142
148
|
opt.unwrapOr(0);
|
|
143
149
|
|
|
144
150
|
// nalloc - same safety, zero allocations
|
|
145
151
|
import { Option, Result, ok } from 'nalloc';
|
|
146
|
-
const result = ok(42);
|
|
147
|
-
Result.map(result, x => x * 2);
|
|
148
|
-
Option.unwrapOr(42, 0);
|
|
152
|
+
const result = ok(42); // no wrapper
|
|
153
|
+
Result.map(result, (x) => x * 2);
|
|
154
|
+
Option.unwrapOr(42, 0); // value IS the Option
|
|
149
155
|
```
|
|
150
156
|
|
|
151
157
|
### From Rust
|
|
@@ -156,16 +162,16 @@ The API mirrors Rust's `Option` and `Result`:
|
|
|
156
162
|
import { Option, Result, ok, err, none, gen } from 'nalloc';
|
|
157
163
|
|
|
158
164
|
// Rust: Some(42).map(|x| x * 2)
|
|
159
|
-
Option.map(42, x => x * 2);
|
|
165
|
+
Option.map(42, (x) => x * 2);
|
|
160
166
|
|
|
161
167
|
// Rust: Ok(42).and_then(|x| if x > 0 { Ok(x) } else { Err("negative") })
|
|
162
|
-
Result.andThen(ok(42), x => x > 0 ? ok(x) : err('negative'));
|
|
168
|
+
Result.andThen(ok(42), (x) => (x > 0 ? ok(x) : err('negative')));
|
|
163
169
|
|
|
164
170
|
// Rust: result.unwrap_or(0)
|
|
165
171
|
Result.unwrapOr(result, 0);
|
|
166
172
|
|
|
167
173
|
// Rust: let value = get_value()?
|
|
168
|
-
const value = gen(function*($) {
|
|
174
|
+
const value = gen(function* ($) {
|
|
169
175
|
const a = yield* $(getValue());
|
|
170
176
|
return a + 1;
|
|
171
177
|
});
|
|
@@ -182,8 +188,8 @@ import { Option, none } from 'nalloc';
|
|
|
182
188
|
const maybePort = Option.fromNullable(process.env.PORT);
|
|
183
189
|
|
|
184
190
|
// Transform
|
|
185
|
-
const doubled = Option.map(maybePort, p => parseInt(p) * 2);
|
|
186
|
-
const validated = Option.filter(doubled, n => n > 0);
|
|
191
|
+
const doubled = Option.map(maybePort, (p) => parseInt(p) * 2);
|
|
192
|
+
const validated = Option.filter(doubled, (n) => n > 0);
|
|
187
193
|
|
|
188
194
|
// Extract
|
|
189
195
|
const port = Option.unwrapOr(maybePort, '3000');
|
|
@@ -191,17 +197,15 @@ const port = Option.unwrapOr(maybePort, '3000');
|
|
|
191
197
|
// Pattern match
|
|
192
198
|
const label = Option.match(
|
|
193
199
|
maybePort,
|
|
194
|
-
value => `Port: ${value}`,
|
|
195
|
-
() => 'Port: default'
|
|
200
|
+
(value) => `Port: ${value}`,
|
|
201
|
+
() => 'Port: default',
|
|
196
202
|
);
|
|
197
203
|
|
|
198
204
|
// Assert and narrow
|
|
199
205
|
Option.assertSome(maybePort, 'PORT is required');
|
|
200
206
|
|
|
201
207
|
// Filter collections
|
|
202
|
-
const activeIds = Option.filterMap(users, user =>
|
|
203
|
-
user.isActive ? user.id : none
|
|
204
|
-
);
|
|
208
|
+
const activeIds = Option.filterMap(users, (user) => (user.isActive ? user.id : none));
|
|
205
209
|
|
|
206
210
|
// Async
|
|
207
211
|
const maybeUser = await Option.fromPromise(fetchUserById(id));
|
|
@@ -212,58 +216,61 @@ const maybeUser = await Option.fromPromise(fetchUserById(id));
|
|
|
212
216
|
A `Result<T, E>` is either `Ok<T>` (the value itself) or `Err<E>` (a wrapper).
|
|
213
217
|
|
|
214
218
|
```ts
|
|
215
|
-
import { Result, ok, err, gen, genAsync, pipe } from 'nalloc';
|
|
219
|
+
import { Result, Schema, ok, err, gen, genAsync, pipe } from 'nalloc';
|
|
216
220
|
|
|
217
221
|
// Wrap throwing functions
|
|
218
222
|
const parsed = Result.tryCatch(
|
|
219
223
|
() => JSON.parse(raw),
|
|
220
|
-
e => 'invalid json'
|
|
224
|
+
(e) => 'invalid json',
|
|
221
225
|
);
|
|
222
226
|
|
|
223
227
|
// gen: Rust-like ? operator with preserved error types
|
|
224
|
-
const result = gen(function*($) {
|
|
228
|
+
const result = gen(function* ($) {
|
|
225
229
|
const config = yield* $(parseConfig(raw));
|
|
226
230
|
const db = yield* $(connectDb(config.url));
|
|
227
231
|
return db.query('SELECT 1');
|
|
228
232
|
}); // Result<QueryResult, ConfigError | DbError>
|
|
229
233
|
|
|
230
234
|
// Async gen
|
|
231
|
-
const data = await genAsync(async function*($) {
|
|
235
|
+
const data = await genAsync(async function* ($) {
|
|
232
236
|
const res = yield* $(await Result.fromPromise(fetchUser(id)));
|
|
233
237
|
const posts = yield* $(await Result.fromPromise(fetchPosts(res.id)));
|
|
234
238
|
return { user: res, posts };
|
|
235
239
|
}); // Promise<Result<{user, posts}, unknown>>
|
|
236
240
|
|
|
237
241
|
// wrap / toThrowable: ecosystem boundaries
|
|
238
|
-
const safeParse = Result.wrap(JSON.parse);
|
|
239
|
-
safeParse('{"a":1}');
|
|
240
|
-
const throwingFind = Result.toThrowable(findUser);
|
|
241
|
-
throwingFind('123');
|
|
242
|
+
const safeParse = Result.wrap(JSON.parse); // throwing -> Result
|
|
243
|
+
safeParse('{"a":1}'); // Ok({a: 1})
|
|
244
|
+
const throwingFind = Result.toThrowable(findUser); // Result -> throwing
|
|
245
|
+
throwingFind('123'); // returns User or throws
|
|
242
246
|
|
|
243
247
|
// Standard Schema validation (Zod, Valibot, ArkType, etc.)
|
|
244
|
-
const validated =
|
|
248
|
+
const validated = Schema.fromSchema(userSchema, input);
|
|
245
249
|
// Result<User, readonly SchemaIssue[]>
|
|
246
250
|
|
|
251
|
+
// wrapSchema: bind a schema once, reuse the validator
|
|
252
|
+
const parseUser = Schema.wrapSchema(userSchema);
|
|
253
|
+
|
|
247
254
|
// Transform with pipe
|
|
248
255
|
const userId = pipe(
|
|
249
256
|
parsed,
|
|
250
|
-
r => Result.map(r, data => data.userId),
|
|
251
|
-
r => Result.flatMap(r, id => id > 0 ? ok(id) : err('invalid id')),
|
|
257
|
+
(r) => Result.map(r, (data) => data.userId),
|
|
258
|
+
(r) => Result.flatMap(r, (id) => (id > 0 ? ok(id) : err('invalid id'))),
|
|
252
259
|
);
|
|
253
260
|
|
|
254
261
|
// Pattern match
|
|
255
262
|
const user = Result.match(
|
|
256
263
|
parsed,
|
|
257
|
-
data => data.user,
|
|
258
|
-
error => null
|
|
264
|
+
(data) => data.user,
|
|
265
|
+
(error) => null,
|
|
259
266
|
);
|
|
260
267
|
|
|
261
268
|
// Assert and narrow
|
|
262
269
|
Result.assertOk(loaded, 'Config required');
|
|
263
270
|
|
|
264
271
|
// Collections
|
|
265
|
-
const combined = Result.all([ok(1), ok(2), ok(3)]);
|
|
266
|
-
const first = Result.any([err('a'), ok(42), err('b')]);
|
|
272
|
+
const combined = Result.all([ok(1), ok(2), ok(3)]); // Ok([1, 2, 3])
|
|
273
|
+
const first = Result.any([err('a'), ok(42), err('b')]); // Ok(42)
|
|
267
274
|
const [oks, errs] = Result.partition(results);
|
|
268
275
|
```
|
|
269
276
|
|
|
@@ -281,9 +288,7 @@ for (const result of Iter.safeIter(riskyIterator)) {
|
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
// mapWhile: yields mapped values while fn returns Some
|
|
284
|
-
const taken = [...Iter.mapWhile([1, 2, 3, 4, 5], n =>
|
|
285
|
-
n < 4 ? n * 10 : null
|
|
286
|
-
)]; // [10, 20, 30]
|
|
291
|
+
const taken = [...Iter.mapWhile([1, 2, 3, 4, 5], (n) => (n < 4 ? n * 10 : null))]; // [10, 20, 30]
|
|
287
292
|
|
|
288
293
|
// tryCollect: collect Results into Result<T[], E>
|
|
289
294
|
const collected = Iter.tryCollect(results); // Ok([1, 2, 3]) or first Err
|
|
@@ -292,7 +297,7 @@ const collected = Iter.tryCollect(results); // Ok([1, 2, 3]) or first Err
|
|
|
292
297
|
const sum = Iter.tryFold([1, 2, 3], 0, (acc, n) => ok(acc + n));
|
|
293
298
|
|
|
294
299
|
// tryForEach: iterate with early exit on Err
|
|
295
|
-
Iter.tryForEach(items, item => processItem(item));
|
|
300
|
+
Iter.tryForEach(items, (item) => processItem(item));
|
|
296
301
|
```
|
|
297
302
|
|
|
298
303
|
## NonEmpty
|
|
@@ -304,8 +309,8 @@ the original array value when it is non-empty.
|
|
|
304
309
|
```ts
|
|
305
310
|
import { NonEmpty, Option } from 'nalloc';
|
|
306
311
|
|
|
307
|
-
NonEmpty.isNonEmpty([])
|
|
308
|
-
NonEmpty.isNonEmpty([1])
|
|
312
|
+
NonEmpty.isNonEmpty([]); // false
|
|
313
|
+
NonEmpty.isNonEmpty([1]); // true, narrows to ReadonlyNonEmptyArray<number>
|
|
309
314
|
|
|
310
315
|
const values: readonly number[] = loadValues();
|
|
311
316
|
if (NonEmpty.isNonEmpty(values)) {
|
|
@@ -327,134 +332,249 @@ Two Result combinators now carry the non-empty guarantee in their error type:
|
|
|
327
332
|
|
|
328
333
|
### Option
|
|
329
334
|
|
|
330
|
-
| Function
|
|
331
|
-
|
|
332
|
-
| `fromNullable(value)`
|
|
333
|
-
| `fromPromise(promise)`
|
|
334
|
-
| `map(opt, fn)`
|
|
335
|
-
| `flatMap(opt, fn)`
|
|
336
|
-
| `andThen(opt, fn)`
|
|
337
|
-
| `tap(opt, fn)`
|
|
338
|
-
| `tapNone(opt, fn)`
|
|
339
|
-
| `filter(opt, predicate)`
|
|
340
|
-
| `match(opt, onSome, onNone)`
|
|
341
|
-
| `unwrap(opt)`
|
|
342
|
-
| `unwrapOr(opt, default)`
|
|
343
|
-
| `unwrapOrElse(opt, fn)`
|
|
344
|
-
| `unwrapOrReturn(opt, fn)`
|
|
345
|
-
| `expect(opt, msg)`
|
|
346
|
-
| `assertSome(opt, msg?)`
|
|
347
|
-
| `isSome(opt)` / `isNone(opt)`
|
|
348
|
-
| `isSomeAnd(opt, pred)`
|
|
349
|
-
| `isNoneOr(opt, pred)`
|
|
350
|
-
| `or(opt, other)`
|
|
351
|
-
| `orElse(opt, fn)`
|
|
352
|
-
| `and(opt, other)`
|
|
353
|
-
| `xor(opt, other)`
|
|
354
|
-
| `zip(opt, other)`
|
|
355
|
-
| `unzip(opt)`
|
|
356
|
-
| `flatten(opt)`
|
|
357
|
-
| `contains(opt, value)`
|
|
358
|
-
| `mapOr(opt, default, fn)`
|
|
359
|
-
| `mapOrElse(opt, defaultFn, fn)` | Map or compute default
|
|
360
|
-
| `toArray(opt)`
|
|
361
|
-
| `toNullable(opt)`
|
|
362
|
-
| `toUndefined(opt)`
|
|
363
|
-
| `okOr(opt, error)`
|
|
364
|
-
| `okOrElse(opt, fn)`
|
|
365
|
-
| `ofOk(result)`
|
|
366
|
-
| `ofErr(result)`
|
|
367
|
-
| `filterMap(items, fn)`
|
|
368
|
-
| `findMap(items, fn)`
|
|
335
|
+
| Function | Description |
|
|
336
|
+
| ------------------------------- | ------------------------------------ |
|
|
337
|
+
| `fromNullable(value)` | Convert nullable to Option |
|
|
338
|
+
| `fromPromise(promise)` | Promise to Option (rejection = None) |
|
|
339
|
+
| `map(opt, fn)` | Transform Some value |
|
|
340
|
+
| `flatMap(opt, fn)` | Chain Option-returning functions |
|
|
341
|
+
| `andThen(opt, fn)` | Alias for flatMap |
|
|
342
|
+
| `tap(opt, fn)` | Side effect on Some, return original |
|
|
343
|
+
| `tapNone(opt, fn)` | Side effect on None, return original |
|
|
344
|
+
| `filter(opt, predicate)` | Keep Some if predicate passes |
|
|
345
|
+
| `match(opt, onSome, onNone)` | Pattern match |
|
|
346
|
+
| `unwrap(opt)` | Extract or throw |
|
|
347
|
+
| `unwrapOr(opt, default)` | Extract or default |
|
|
348
|
+
| `unwrapOrElse(opt, fn)` | Extract or compute default |
|
|
349
|
+
| `unwrapOrReturn(opt, fn)` | Extract or return computed value |
|
|
350
|
+
| `expect(opt, msg)` | Extract or throw with message |
|
|
351
|
+
| `assertSome(opt, msg?)` | Assert and narrow to Some |
|
|
352
|
+
| `isSome(opt)` / `isNone(opt)` | Type guards |
|
|
353
|
+
| `isSomeAnd(opt, pred)` | Some and predicate passes |
|
|
354
|
+
| `isNoneOr(opt, pred)` | None or predicate passes |
|
|
355
|
+
| `or(opt, other)` | Return first Some |
|
|
356
|
+
| `orElse(opt, fn)` | Return Some or compute fallback |
|
|
357
|
+
| `and(opt, other)` | Return second if first is Some |
|
|
358
|
+
| `xor(opt, other)` | Some if exactly one is Some |
|
|
359
|
+
| `zip(opt, other)` | Combine two Options into tuple |
|
|
360
|
+
| `unzip(opt)` | Split tuple Option into two |
|
|
361
|
+
| `flatten(opt)` | Flatten nested Option |
|
|
362
|
+
| `contains(opt, value)` | Check if Some contains value |
|
|
363
|
+
| `mapOr(opt, default, fn)` | Map or return default |
|
|
364
|
+
| `mapOrElse(opt, defaultFn, fn)` | Map or compute default |
|
|
365
|
+
| `toArray(opt)` | Some to [value], None to [] |
|
|
366
|
+
| `toNullable(opt)` | Some to value, None to null |
|
|
367
|
+
| `toUndefined(opt)` | Some to value, None to undefined |
|
|
368
|
+
| `okOr(opt, error)` | Option to Result |
|
|
369
|
+
| `okOrElse(opt, fn)` | Option to Result with computed error |
|
|
370
|
+
| `ofOk(result)` | Ok to Some, Err to None |
|
|
371
|
+
| `ofErr(result)` | Err to Some, Ok to None |
|
|
372
|
+
| `filterMap(items, fn)` | Map and filter in one pass |
|
|
373
|
+
| `findMap(items, fn)` | Find first Some from mapping |
|
|
369
374
|
|
|
370
375
|
### Result
|
|
371
376
|
|
|
372
|
-
| Function
|
|
373
|
-
|
|
374
|
-
| `tryCatch(fn, onError?)`
|
|
375
|
-
| `tryCatchMaybePromise(fn, onError?)` | Wrap sync-or-async, preserving sync
|
|
376
|
-
| `of(fn)`
|
|
377
|
-
| `wrap(fn, onError?)`
|
|
378
|
-
| `toThrowable(fn)`
|
|
379
|
-
| `
|
|
380
|
-
| `
|
|
381
|
-
| `
|
|
382
|
-
| `
|
|
383
|
-
| `
|
|
384
|
-
| `
|
|
385
|
-
| `
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
390
|
-
| `
|
|
391
|
-
| `
|
|
392
|
-
| `
|
|
393
|
-
| `
|
|
394
|
-
| `
|
|
395
|
-
| `
|
|
396
|
-
| `
|
|
397
|
-
| `
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
400
|
-
| `
|
|
401
|
-
| `
|
|
402
|
-
| `
|
|
403
|
-
| `
|
|
404
|
-
| `
|
|
405
|
-
| `
|
|
406
|
-
| `
|
|
407
|
-
| `
|
|
408
|
-
| `
|
|
409
|
-
| `
|
|
410
|
-
| `
|
|
411
|
-
| `
|
|
412
|
-
| `
|
|
413
|
-
| `
|
|
414
|
-
| `
|
|
415
|
-
| `
|
|
416
|
-
| `
|
|
417
|
-
| `
|
|
418
|
-
| `
|
|
419
|
-
| `
|
|
420
|
-
| `
|
|
421
|
-
| `
|
|
422
|
-
| `
|
|
423
|
-
| `
|
|
424
|
-
| `
|
|
425
|
-
|
|
377
|
+
| Function | Description |
|
|
378
|
+
| ------------------------------------ | --------------------------------------------- |
|
|
379
|
+
| `tryCatch(fn, onError?)` | Wrap throwing function |
|
|
380
|
+
| `tryCatchMaybePromise(fn, onError?)` | Wrap sync-or-async, preserving sync |
|
|
381
|
+
| `of(fn)` | Alias for tryCatch (no error mapper) |
|
|
382
|
+
| `wrap(fn, onError?)` | Wrap throwing function once, reuse |
|
|
383
|
+
| `toThrowable(fn)` | Inverse of wrap: Result-returning to throwing |
|
|
384
|
+
| `gen(fn)` | Generator do-notation with typed errors |
|
|
385
|
+
| `genAsync(fn)` | Async generator do-notation |
|
|
386
|
+
| `safeTry(fn)` | Imperative error handling with unwrap |
|
|
387
|
+
| `safeTryAsync(fn)` | Async version of safeTry |
|
|
388
|
+
| `unwrap(result)` | Extract Ok or throw error value |
|
|
389
|
+
| `unwrapErr(result)` | Extract Err or throw |
|
|
390
|
+
| `unwrapOr(result, default)` | Extract Ok or default |
|
|
391
|
+
| `unwrapOrElse(result, fn)` | Extract Ok or compute from error |
|
|
392
|
+
| `unwrapOrReturn(result, fn)` | Extract Ok or return computed value |
|
|
393
|
+
| `expect(result, msg)` | Extract Ok or throw with message |
|
|
394
|
+
| `expectErr(result, msg)` | Extract Err or throw with message |
|
|
395
|
+
| `map(result, fn)` | Transform Ok value |
|
|
396
|
+
| `mapErr(result, fn)` | Transform Err value |
|
|
397
|
+
| `bimap(result, okFn, errFn)` | Transform both Ok and Err |
|
|
398
|
+
| `flatMap(result, fn)` | Chain Result-returning functions |
|
|
399
|
+
| `andThen(result, fn)` | Alias for flatMap |
|
|
400
|
+
| `tap(result, fn)` | Side effect on Ok, return original |
|
|
401
|
+
| `tapErr(result, fn)` | Side effect on Err, return original |
|
|
402
|
+
| `match(result, onOk, onErr)` | Pattern match |
|
|
403
|
+
| `assertOk(result, msg?)` | Assert and narrow to Ok |
|
|
404
|
+
| `assertErr(result, msg?)` | Assert and narrow to Err |
|
|
405
|
+
| `isOk(result)` / `isErr(result)` | Type guards |
|
|
406
|
+
| `isOkAnd(result, pred)` | Ok and predicate passes |
|
|
407
|
+
| `isErrAnd(result, pred)` | Err and predicate passes |
|
|
408
|
+
| `isSomeErr(result)` | Err with non-null error |
|
|
409
|
+
| `and(result, other)` | Return second if first is Ok |
|
|
410
|
+
| `or(result, other)` | Return first Ok |
|
|
411
|
+
| `orElse(result, fn)` | Ok or compute fallback from error |
|
|
412
|
+
| `zip(a, b)` | Combine two Ok into tuple |
|
|
413
|
+
| `zipWith(a, b, fn)` | Combine two Ok with function |
|
|
414
|
+
| `flatten(result)` | Flatten nested Result |
|
|
415
|
+
| `transpose(result)` | Result<Option> to Option<Result> |
|
|
416
|
+
| `toOption(result)` | Ok to Some, Err to None |
|
|
417
|
+
| `toErrorOption(result)` | Err to Some, Ok to None |
|
|
418
|
+
| `mapOr(result, default, fn)` | Map Ok or return default |
|
|
419
|
+
| `mapOrElse(result, defaultFn, fn)` | Map Ok or compute default |
|
|
420
|
+
| `all(results)` | Collect all Ok or first Err |
|
|
421
|
+
| `any(results)` | First Ok or all Errs |
|
|
422
|
+
| `collect(results)` | Collect Ok values or first Err |
|
|
423
|
+
| `collectAll(results)` | All Ok or all Errs |
|
|
424
|
+
| `partition(results)` | Split into [oks, errs] |
|
|
425
|
+
| `partitionAsync(promises)` | Async partition |
|
|
426
|
+
| `filterOk(results)` | Extract all Ok values |
|
|
427
|
+
| `filterErr(results)` | Extract all Err values |
|
|
428
|
+
| `settleMaybePromise(values)` | Settle sync/async values to Results |
|
|
429
|
+
| `partitionMaybePromise(values)` | Partition sync/async Results |
|
|
430
|
+
|
|
431
|
+
### Schema
|
|
432
|
+
|
|
433
|
+
Standard Schema v1 validation. Tree-shakeable: import the `Schema` namespace from `nalloc`, or directly from `nalloc/schema`.
|
|
434
|
+
|
|
435
|
+
| Function | Description |
|
|
436
|
+
| --------------------------- | ------------------------------------------------ |
|
|
437
|
+
| `fromSchema(schema, value)` | Validate via Standard Schema v1, returns Result |
|
|
438
|
+
| `wrapSchema(schema)` | Bind a schema once, returns a reusable validator |
|
|
439
|
+
|
|
440
|
+
### HTTP
|
|
441
|
+
|
|
442
|
+
Optional `nalloc/http` subpath. Works in any runtime with `fetch` (browser, Node 18+, Bun, Deno, edge workers); kept out of the main entry so the core stays free of environment-typed globals.
|
|
443
|
+
|
|
444
|
+
Native `fetch` resolves successfully on 4xx/5xx - only transport failures reject. So the generic helpers alone let an HTTP error flow down the Ok path looking like success:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
const r = await Result.fromPromise(fetch(url)); // Ok(Response { status: 404 }) - not an Err!
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`fromFetch` closes that gap: Ok means the request connected AND returned 2xx, and every failure mode lands in the error channel, typed:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
import { fromFetch } from 'nalloc/http';
|
|
454
|
+
|
|
455
|
+
const res = await fromFetch(url);
|
|
456
|
+
// Result<Response, Response | TypeError | DOMException>
|
|
457
|
+
// Ok(Response) -> connected and 2xx
|
|
458
|
+
// Err(Response) -> reached the server, non-2xx (read status/body from it)
|
|
459
|
+
// Err(TypeError) -> network/CORS failure
|
|
460
|
+
// Err(DOMException) -> aborted or timed out
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Already holding a `Response` (custom fetch wrapper, middleware, a different HTTP client)? `fromResponse` is the status check alone, and composes with `fromPromise` if you need the two steps separately:
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
import { fromResponse } from 'nalloc/http';
|
|
467
|
+
|
|
468
|
+
const checked = fromResponse(response); // Ok(response) if response.ok, else Err(response)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
| Function | Description |
|
|
472
|
+
| ------------------------- | -------------------------------------------------------------------- |
|
|
473
|
+
| `fromFetch(input, init?)` | Run fetch: Ok(response) for 2xx, Err on non-2xx or transport failure |
|
|
474
|
+
| `fromResponse(response)` | Ok(response) if response.ok, else Err(response) |
|
|
426
475
|
|
|
427
476
|
### Iter
|
|
428
477
|
|
|
429
|
-
| Function
|
|
430
|
-
|
|
431
|
-
| `safeIter(source)`
|
|
432
|
-
| `mapWhile(source, fn)`
|
|
433
|
-
| `tryCollect(source)`
|
|
434
|
-
| `tryFold(source, init, fn)` | Fold with early exit on Err
|
|
435
|
-
| `tryForEach(source, fn)`
|
|
478
|
+
| Function | Description |
|
|
479
|
+
| --------------------------- | ------------------------------------------ |
|
|
480
|
+
| `safeIter(source)` | Wrap iterable: values as Ok, throws as Err |
|
|
481
|
+
| `mapWhile(source, fn)` | Yield mapped values while fn returns Some |
|
|
482
|
+
| `tryCollect(source)` | Collect Results into Result<T[], E> |
|
|
483
|
+
| `tryFold(source, init, fn)` | Fold with early exit on Err |
|
|
484
|
+
| `tryForEach(source, fn)` | Iterate with early exit on Err |
|
|
436
485
|
|
|
437
486
|
### Utilities
|
|
438
487
|
|
|
439
|
-
| Function
|
|
440
|
-
|
|
488
|
+
| Function | Description |
|
|
489
|
+
| --------------------- | -------------------------------------------- |
|
|
441
490
|
| `pipe(value, ...fns)` | Thread value through functions left-to-right |
|
|
442
491
|
|
|
492
|
+
## ESLint plugin
|
|
493
|
+
|
|
494
|
+
nalloc ships an optional ESLint plugin (flat config) at `nalloc/eslint`. It is dev-time only and
|
|
495
|
+
never enters your runtime bundle. The typescript-eslint toolchain is an optional peer - install it
|
|
496
|
+
alongside ESLint:
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
npm install -D eslint @typescript-eslint/parser @typescript-eslint/utils typescript
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
```js
|
|
503
|
+
// eslint.config.js
|
|
504
|
+
import nalloc from 'nalloc/eslint';
|
|
505
|
+
import tsParser from '@typescript-eslint/parser';
|
|
506
|
+
|
|
507
|
+
export default [
|
|
508
|
+
{
|
|
509
|
+
files: ['**/*.ts'],
|
|
510
|
+
languageOptions: {
|
|
511
|
+
parser: tsParser,
|
|
512
|
+
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
|
|
513
|
+
},
|
|
514
|
+
plugins: { nalloc },
|
|
515
|
+
rules: {
|
|
516
|
+
'nalloc/must-use': 'error',
|
|
517
|
+
'nalloc/no-unwrap': 'error',
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
{ files: ['**/*.test.ts'], rules: { 'nalloc/no-unwrap': 'off' } },
|
|
521
|
+
];
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
| Rule | What it flags | Type info |
|
|
525
|
+
| ------------------ | ----------------------------------------------------------------------- | ----------------------------------------- |
|
|
526
|
+
| `nalloc/must-use` | A `Result`/`Option` value used as a bare statement and discarded | Required (`parserOptions.projectService`) |
|
|
527
|
+
| `nalloc/no-unwrap` | `unwrap` / `unwrapErr` / `expect` / `expectErr`, which throw on failure | Not required |
|
|
528
|
+
|
|
529
|
+
`nalloc.configs.recommended` turns both rules on; it does not configure the parser, so a type-aware
|
|
530
|
+
setup for `must-use` is still your config's responsibility. Without type info, `must-use` reports the
|
|
531
|
+
standard typescript-eslint parserServices error while `no-unwrap` keeps working.
|
|
532
|
+
|
|
533
|
+
## Codemod: migrate from neverthrow
|
|
534
|
+
|
|
535
|
+
nalloc bundles a best-effort codemod that rewrites neverthrow code to nalloc. It needs the
|
|
536
|
+
optional `oxc-parser` peer (dev-time only, never in your runtime bundle):
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
npm install -D oxc-parser
|
|
540
|
+
npx nalloc-codemod src --report migration-report.md # add --dry to preview
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
What it converts: the neverthrow import (aliases preserved); method calls to function calls
|
|
544
|
+
(`r.map(f)` -> `Result.map(r, f)`, `.andThen` -> `Result.flatMap`, `.mapErr`, `.orElse`, `.match`,
|
|
545
|
+
`.unwrapOr`, `.isOk`/`.isErr`, `.andTee`/`.orTee` -> `tap`/`tapErr`, `_unsafeUnwrap` -> `Result.unwrap`);
|
|
546
|
+
chains of two or more methods fold into `pipe(...)`; `fromThrowable` -> `Result.wrap`;
|
|
547
|
+
`combine` -> `Result.all`; `combineWithAllErrors` -> `Result.collectAll`; directly-awaited
|
|
548
|
+
`fromPromise` -> `Result.fromPromise`; `Result<T, E>` type annotations -> `ResultType<T, E>`.
|
|
549
|
+
Edits are text splices, so untouched code keeps its formatting byte for byte.
|
|
550
|
+
|
|
551
|
+
What it refuses and reports instead: `ResultAsync` and everything that produces one
|
|
552
|
+
(`okAsync`, `errAsync`, chained `fromPromise`, `asyncMap`, ...) - the nalloc idiom is `genAsync`,
|
|
553
|
+
which is a manual rewrite; `safeTry` generators (use `gen`); namespace imports
|
|
554
|
+
(`import * as nt`). Those keep a residual neverthrow import so the file still compiles mid-migration.
|
|
555
|
+
|
|
556
|
+
Two safety properties to know. Conversion is provenance-based: ambiguous method names like `.map`
|
|
557
|
+
are only rewritten when the receiver provably holds a Result (constructed from `ok`/`err`/`combine`/
|
|
558
|
+
a `fromThrowable`-wrapped function, annotated with a neverthrow type, or chained with
|
|
559
|
+
neverthrow-only methods) - a bare `.map` on an unknown receiver is assumed to be an array and left
|
|
560
|
+
alone. And migrate the whole project in one run: converted call sites assume their values are
|
|
561
|
+
nalloc values, which only holds once the constructors producing them are converted too.
|
|
562
|
+
|
|
443
563
|
## Comparison
|
|
444
564
|
|
|
445
|
-
| Feature
|
|
446
|
-
|
|
447
|
-
| Zero-alloc Option
|
|
448
|
-
| Zero-alloc Ok
|
|
449
|
-
| Bundle size
|
|
450
|
-
| Learning curve
|
|
451
|
-
| Async support
|
|
452
|
-
| Tree-shakeable
|
|
453
|
-
| Iterator utilities | Yes
|
|
454
|
-
| gen (? operator)
|
|
455
|
-
| pipe
|
|
456
|
-
| Schema validation
|
|
457
|
-
| Ecosystem interop
|
|
565
|
+
| Feature | nalloc | neverthrow | fp-ts | oxide.ts | ts-results |
|
|
566
|
+
| ------------------ | ------ | ---------- | ----- | -------- | ---------- |
|
|
567
|
+
| Zero-alloc Option | Yes | No | No | No | No |
|
|
568
|
+
| Zero-alloc Ok | Yes | No | No | No | No |
|
|
569
|
+
| Bundle size | Tiny | Small | Large | Small | Small |
|
|
570
|
+
| Learning curve | Low | Low | High | Low | Low |
|
|
571
|
+
| Async support | Yes | Yes | Yes | Limited | No |
|
|
572
|
+
| Tree-shakeable | Yes | Yes | Yes | Yes | Yes |
|
|
573
|
+
| Iterator utilities | Yes | No | Yes | No | No |
|
|
574
|
+
| gen (? operator) | Yes | Yes | No | No | No |
|
|
575
|
+
| pipe | Yes | No | Yes | No | No |
|
|
576
|
+
| Schema validation | Yes | No | No | No | No |
|
|
577
|
+
| Ecosystem interop | Yes | No | No | No | No |
|
|
458
578
|
|
|
459
579
|
## Alternatives
|
|
460
580
|
|