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.
Files changed (50) hide show
  1. package/README.md +300 -180
  2. package/build/codemod-cli.cjs +153 -0
  3. package/build/codemod-cli.cjs.map +1 -0
  4. package/build/codemod-cli.d.ts +2 -0
  5. package/build/codemod-cli.js +103 -0
  6. package/build/codemod-cli.js.map +1 -0
  7. package/build/codemod.cjs +652 -0
  8. package/build/codemod.cjs.map +1 -0
  9. package/build/codemod.d.ts +29 -0
  10. package/build/codemod.js +634 -0
  11. package/build/codemod.js.map +1 -0
  12. package/build/eslint.cjs +221 -0
  13. package/build/eslint.cjs.map +1 -0
  14. package/build/eslint.d.ts +36 -0
  15. package/build/eslint.js +198 -0
  16. package/build/eslint.js.map +1 -0
  17. package/build/http.cjs +31 -0
  18. package/build/http.cjs.map +1 -0
  19. package/build/http.d.ts +31 -0
  20. package/build/http.js +13 -0
  21. package/build/http.js.map +1 -0
  22. package/build/result.cjs +0 -11
  23. package/build/result.cjs.map +1 -1
  24. package/build/result.d.ts +0 -32
  25. package/build/result.js +0 -8
  26. package/build/result.js.map +1 -1
  27. package/build/safe.cjs +4 -0
  28. package/build/safe.cjs.map +1 -1
  29. package/build/safe.d.ts +1 -0
  30. package/build/safe.js +1 -0
  31. package/build/safe.js.map +1 -1
  32. package/build/schema.cjs +32 -0
  33. package/build/schema.cjs.map +1 -0
  34. package/build/schema.d.ts +44 -0
  35. package/build/schema.js +14 -0
  36. package/build/schema.js.map +1 -0
  37. package/package.json +54 -5
  38. package/src/__tests__/codemod.ts +211 -0
  39. package/src/__tests__/eslint.ts +99 -0
  40. package/src/__tests__/fixtures/tsconfig.json +10 -0
  41. package/src/__tests__/http.ts +64 -0
  42. package/src/__tests__/result.ts +74 -125
  43. package/src/__tests__/schema.ts +58 -0
  44. package/src/codemod-cli.ts +108 -0
  45. package/src/codemod.ts +623 -0
  46. package/src/eslint.ts +145 -0
  47. package/src/http.ts +42 -0
  48. package/src/result.ts +0 -37
  49. package/src/safe.ts +1 -0
  50. 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 | 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 |
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(() => JSON.parse(raw), () => 'invalid json');
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 | Representation | Allocation |
79
- |------|----------------|------------|
80
- | `Option<T>` | `T \| null \| undefined` | Zero |
81
- | `Some<T>` | The value itself | Zero |
82
- | `None` | `null` or `undefined` | Zero |
83
- | `Ok<T>` | The value itself (branded) | Zero |
84
- | `Err<E>` | Minimal wrapper `{ error: E }` | One object |
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); // zero allocation
101
- Result.map(result, x => x * 2); // function-based API
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(O.some(42), O.map(x => x * 2));
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); // 42 is the Option itself
130
- const result = ok(42); // zero allocation
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); // no wrapper
147
- Result.map(result, x => x * 2);
148
- Option.unwrapOr(42, 0); // value IS the Option
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); // throwing -> Result
239
- safeParse('{"a":1}'); // Ok({a: 1})
240
- const throwingFind = Result.toThrowable(findUser); // Result -> throwing
241
- throwingFind('123'); // returns User or throws
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 = Result.fromSchema(userSchema, input);
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)]); // Ok([1, 2, 3])
266
- const first = Result.any([err('a'), ok(42), err('b')]); // Ok(42)
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([]) // false
308
- NonEmpty.isNonEmpty([1]) // true, narrows to ReadonlyNonEmptyArray<number>
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 | Description |
331
- |----------|-------------|
332
- | `fromNullable(value)` | Convert nullable to Option |
333
- | `fromPromise(promise)` | Promise to Option (rejection = None) |
334
- | `map(opt, fn)` | Transform Some value |
335
- | `flatMap(opt, fn)` | Chain Option-returning functions |
336
- | `andThen(opt, fn)` | Alias for flatMap |
337
- | `tap(opt, fn)` | Side effect on Some, return original |
338
- | `tapNone(opt, fn)` | Side effect on None, return original |
339
- | `filter(opt, predicate)` | Keep Some if predicate passes |
340
- | `match(opt, onSome, onNone)` | Pattern match |
341
- | `unwrap(opt)` | Extract or throw |
342
- | `unwrapOr(opt, default)` | Extract or default |
343
- | `unwrapOrElse(opt, fn)` | Extract or compute default |
344
- | `unwrapOrReturn(opt, fn)` | Extract or return computed value |
345
- | `expect(opt, msg)` | Extract or throw with message |
346
- | `assertSome(opt, msg?)` | Assert and narrow to Some |
347
- | `isSome(opt)` / `isNone(opt)` | Type guards |
348
- | `isSomeAnd(opt, pred)` | Some and predicate passes |
349
- | `isNoneOr(opt, pred)` | None or predicate passes |
350
- | `or(opt, other)` | Return first Some |
351
- | `orElse(opt, fn)` | Return Some or compute fallback |
352
- | `and(opt, other)` | Return second if first is Some |
353
- | `xor(opt, other)` | Some if exactly one is Some |
354
- | `zip(opt, other)` | Combine two Options into tuple |
355
- | `unzip(opt)` | Split tuple Option into two |
356
- | `flatten(opt)` | Flatten nested Option |
357
- | `contains(opt, value)` | Check if Some contains value |
358
- | `mapOr(opt, default, fn)` | Map or return default |
359
- | `mapOrElse(opt, defaultFn, fn)` | Map or compute default |
360
- | `toArray(opt)` | Some to [value], None to [] |
361
- | `toNullable(opt)` | Some to value, None to null |
362
- | `toUndefined(opt)` | Some to value, None to undefined |
363
- | `okOr(opt, error)` | Option to Result |
364
- | `okOrElse(opt, fn)` | Option to Result with computed error |
365
- | `ofOk(result)` | Ok to Some, Err to None |
366
- | `ofErr(result)` | Err to Some, Ok to None |
367
- | `filterMap(items, fn)` | Map and filter in one pass |
368
- | `findMap(items, fn)` | Find first Some from mapping |
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 | Description |
373
- |----------|-------------|
374
- | `tryCatch(fn, onError?)` | Wrap throwing function |
375
- | `tryCatchMaybePromise(fn, onError?)` | Wrap sync-or-async, preserving sync |
376
- | `of(fn)` | Alias for tryCatch (no error mapper) |
377
- | `wrap(fn, onError?)` | Wrap throwing function once, reuse |
378
- | `toThrowable(fn)` | Inverse of wrap: Result-returning to throwing |
379
- | `fromSchema(schema, value)` | Validate via Standard Schema v1 |
380
- | `gen(fn)` | Generator do-notation with typed errors |
381
- | `genAsync(fn)` | Async generator do-notation |
382
- | `safeTry(fn)` | Imperative error handling with unwrap |
383
- | `safeTryAsync(fn)` | Async version of safeTry |
384
- | `unwrap(result)` | Extract Ok or throw error value |
385
- | `unwrapErr(result)` | Extract Err or throw |
386
- | `unwrapOr(result, default)` | Extract Ok or default |
387
- | `unwrapOrElse(result, fn)` | Extract Ok or compute from error |
388
- | `unwrapOrReturn(result, fn)` | Extract Ok or return computed value |
389
- | `expect(result, msg)` | Extract Ok or throw with message |
390
- | `expectErr(result, msg)` | Extract Err or throw with message |
391
- | `map(result, fn)` | Transform Ok value |
392
- | `mapErr(result, fn)` | Transform Err value |
393
- | `bimap(result, okFn, errFn)` | Transform both Ok and Err |
394
- | `flatMap(result, fn)` | Chain Result-returning functions |
395
- | `andThen(result, fn)` | Alias for flatMap |
396
- | `tap(result, fn)` | Side effect on Ok, return original |
397
- | `tapErr(result, fn)` | Side effect on Err, return original |
398
- | `match(result, onOk, onErr)` | Pattern match |
399
- | `assertOk(result, msg?)` | Assert and narrow to Ok |
400
- | `assertErr(result, msg?)` | Assert and narrow to Err |
401
- | `isOk(result)` / `isErr(result)` | Type guards |
402
- | `isOkAnd(result, pred)` | Ok and predicate passes |
403
- | `isErrAnd(result, pred)` | Err and predicate passes |
404
- | `isSomeErr(result)` | Err with non-null error |
405
- | `and(result, other)` | Return second if first is Ok |
406
- | `or(result, other)` | Return first Ok |
407
- | `orElse(result, fn)` | Ok or compute fallback from error |
408
- | `zip(a, b)` | Combine two Ok into tuple |
409
- | `zipWith(a, b, fn)` | Combine two Ok with function |
410
- | `flatten(result)` | Flatten nested Result |
411
- | `transpose(result)` | Result<Option> to Option<Result> |
412
- | `toOption(result)` | Ok to Some, Err to None |
413
- | `toErrorOption(result)` | Err to Some, Ok to None |
414
- | `mapOr(result, default, fn)` | Map Ok or return default |
415
- | `mapOrElse(result, defaultFn, fn)` | Map Ok or compute default |
416
- | `all(results)` | Collect all Ok or first Err |
417
- | `any(results)` | First Ok or all Errs |
418
- | `collect(results)` | Collect Ok values or first Err |
419
- | `collectAll(results)` | All Ok or all Errs |
420
- | `partition(results)` | Split into [oks, errs] |
421
- | `partitionAsync(promises)` | Async partition |
422
- | `filterOk(results)` | Extract all Ok values |
423
- | `filterErr(results)` | Extract all Err values |
424
- | `settleMaybePromise(values)` | Settle sync/async values to Results |
425
- | `partitionMaybePromise(values)` | Partition sync/async Results |
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 | Description |
430
- |----------|-------------|
431
- | `safeIter(source)` | Wrap iterable: values as Ok, throws as Err |
432
- | `mapWhile(source, fn)` | Yield mapped values while fn returns Some |
433
- | `tryCollect(source)` | Collect Results into Result<T[], E> |
434
- | `tryFold(source, init, fn)` | Fold with early exit on Err |
435
- | `tryForEach(source, fn)` | Iterate with early exit on Err |
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 | Description |
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 | nalloc | neverthrow | fp-ts | oxide.ts | ts-results |
446
- |---------|---------|------------|-------|----------|------------|
447
- | Zero-alloc Option | Yes | No | No | No | No |
448
- | Zero-alloc Ok | Yes | No | No | No | No |
449
- | Bundle size | Tiny | Small | Large | Small | Small |
450
- | Learning curve | Low | Low | High | Low | Low |
451
- | Async support | Yes | Yes | Yes | Limited | No |
452
- | Tree-shakeable | Yes | Yes | Yes | Yes | Yes |
453
- | Iterator utilities | Yes | No | Yes | No | No |
454
- | gen (? operator) | Yes | Yes | No | No | No |
455
- | pipe | Yes | No | Yes | No | No |
456
- | Schema validation | Yes | No | No | No | No |
457
- | Ecosystem interop | Yes | No | No | No | No |
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