@dangayle/rustlike 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,864 @@
1
+ # rustlike
2
+
3
+ TypeScript utilities for writing Rust-like code.
4
+
5
+ ## Why?
6
+
7
+ Rust is great for performance-critical systems work. This library... isn't for that.
8
+
9
+ What it _is_ is a way to learn and apply Rust mental models to TypeScript.
10
+
11
+ Rust enforces computer science principles (explicit error handling, null safety, exhaustive matching, immutability) that most languages leave optional. This library brings those principles to TypeScript: both as a **learning tool** for building Rust mental models and as a **practical library** for writing safer, more predictable code.
12
+
13
+ - **Structs + functions.** Separate data from behavior. No classes, just object literals and functions.
14
+ - **Errors are values.** Return `Result<T, E>` instead of throwing. Exceptions are for panics, not control flow.
15
+ - **No null surprises.** Return `Option<T>` instead of `null`. You can't use a value without handling the absent case first.
16
+ - **Immutability by default.** Data is `readonly`. No mutations hiding in called functions.
17
+ - **Transformation pipelines.** Chain `.map()`, `.andThen()`, `.unwrapOr()` instead of imperative if/else checks.
18
+ - **Exhaustive matching.** Handle every variant of a union. The compiler catches what you miss.
19
+ - **Parse, don't validate.** Use branded types to make invalid states unrepresentable at the type level.
20
+
21
+ **We're not trying to recreate Rust in Typescript.** This library focuses on Rust's _logic handling_ patterns like error propagation, null safety, pattern matching, lazy iterators, and nominal typing. It does not attempt to model Rust's _resource and memory management_ concepts (ownership, borrowing, lifetimes, `Drop`) which don't have meaningful equivalents in a garbage-collected runtime.
22
+
23
+ ## Install
24
+
25
+ Not yet published to npm. Install directly from the repo:
26
+
27
+ ```bash
28
+ pnpm add @dangayle/rustlike
29
+ # or
30
+ npm install @dangayle/rustlike
31
+ ```
32
+
33
+ ## Examples
34
+
35
+ See the [`examples/`](./examples) directory for working demonstrations:
36
+
37
+ - **01-hello-world** - Basic `Option` usage
38
+ - **02-todo-app** - State management with `Result` and `Option`
39
+ - **03-fizzbuzz** - Pattern matching and control flow
40
+ - **04-fibonacci** - Recursive algorithms with memoization
41
+ - **05-button-clicker** - UI state management simulation
42
+ - **06-iris-classification** - k-NN classifier with validation
43
+ - **07-grep-tool** - File I/O and text search
44
+ - **08-fetch-json** - Boundary validation for HTTP JSON responses
45
+ - **09-state-machine** - "Making Invalid States Unrepresentable" using discriminated unions
46
+ - **10-log-analyzer** - Lazy iterators for memory-efficient log file processing
47
+ - **11-sales-report-generator** - Composable data processing with lazy iterator combinators
48
+ - **12-markdown-parser** - CommonMark-subset Markdown-to-HTML parser exercising 19+ rustlike APIs
49
+ - **13-graphql-schema** - GraphQL schema validator exercising `match()`, `assertNever()`, `tryAsync()`, `safeCall()`, and `DeepReadonly`
50
+
51
+ Each example directory contains two implementations:
52
+
53
+ - `idiomatic.ts` - **Standard TypeScript** showing idiomatic TS approaches to solving a problem.
54
+ - `index.ts` - **Rust-like approach** using `rustlike` library patterns, showing the same application re-imagined in a way that is more Rust-like. This lets you see the philosophical differences side-by-side. Run either:
55
+
56
+ ```bash
57
+ cd examples/<example>
58
+ pnpm install
59
+ pnpm start # runs index.ts (rust-like)
60
+ pnpm tsx idiomatic.ts # runs idiomatic version
61
+ ```
62
+
63
+ ## The Rust Mental Model
64
+
65
+ Again, we're not trying to recreate Rust in Typescript. We're trying to adopt the mental model and apply some of its beneficial patterns. This library reinforces specific habits that transfer directly to Rust.
66
+
67
+ ### 1. Errors as Values (`Result`)
68
+
69
+ **Habit:** Exceptions are for unrecoverable system crashes (panics). For everything else (validation, network, missing data), return a `Result`.
70
+
71
+ - **Don't `throw`.** Return `Err()`.
72
+ - **Don't `try/catch`.** Use `Result.fromThrowable()`.
73
+
74
+ **Feature: `Result<T, E>`**
75
+ Error handling without exceptions. Methods chain directly on values, like Rust.
76
+
77
+ ```typescript
78
+ import { Ok, Err, Result } from "rustlike";
79
+
80
+ function divide(a: number, b: number): Result<number, string> {
81
+ if (b === 0) return Err("Cannot divide by zero");
82
+ return Ok(a / b);
83
+ }
84
+
85
+ // Method chaining (Rust-like)
86
+ const value = divide(10, 2)
87
+ .map((x) => x * 2)
88
+ .andThen((x) => divide(x, 2))
89
+ .unwrapOr(0);
90
+
91
+ // Pattern match
92
+ const message = divide(10, 0).match({
93
+ ok: (value) => `Result: ${value}`,
94
+ err: (error) => `Error: ${error}`,
95
+ });
96
+
97
+ // Interop with Option
98
+ const opt = divide(10, 2).toOption(); // Some(5)
99
+
100
+ // Type narrowing still works
101
+ const result = divide(10, 2);
102
+ if (result.isOk()) {
103
+ console.log(result.value); // TypeScript knows this is number
104
+ }
105
+
106
+ // expect() for documenting invariants
107
+ const adminUser = findAdmin().expect("Admin user must exist in database");
108
+
109
+ // Collecting multiple Results
110
+ const items = [1, 2, 3];
111
+ const validated = Result.collect(items.map((x) => validate(x)));
112
+ // Result<ValidatedItem[], Error> - short-circuits on first error
113
+ ```
114
+
115
+ ### 2. No Null Surprises (`Option`)
116
+
117
+ **Habit:** Check First, Then Use. In TypeScript, you can often access a property and get `undefined`. In Rust (and this library), you physically cannot get the value without handling the absent case first.
118
+
119
+ - **Bad Habit.** `if (user && user.id)`
120
+ - **Rust Habit.** `user.map(u => u.id).unwrapOr(default)`
121
+
122
+ **Feature: `Option<T>`**
123
+ Explicit handling of nullable values. Methods chain directly on values.
124
+
125
+ ```typescript
126
+ import { Some, None, Option } from "rustlike";
127
+
128
+ function findUser(id: number): Option<string> {
129
+ return id === 1 ? Some("Alice") : None;
130
+ }
131
+
132
+ // Method chaining (Rust-like)
133
+ const greeting = findUser(1)
134
+ .map((name) => name.toUpperCase())
135
+ .unwrapOr("ANONYMOUS");
136
+
137
+ // Pattern match
138
+ const message = findUser(2).match({
139
+ some: (name) => `Hello, ${name}`,
140
+ none: () => "User not found",
141
+ });
142
+
143
+ // Create from nullable
144
+ const maybeValue = Option.from(possiblyNullValue);
145
+
146
+ // Interop with Result
147
+ const res = findUser(1).okOr("User missing"); // Ok("Alice") or Err("User missing")
148
+
149
+ // Type narrowing still works
150
+ const user = findUser(1);
151
+ if (user.some) {
152
+ console.log(user.value); // TypeScript knows this is string
153
+ }
154
+
155
+ // expect() for documenting invariants
156
+ const config = loadConfig().expect("Config file must be present");
157
+
158
+ // Collecting multiple Options
159
+ const ids = [1, 2, 3];
160
+ const users = Option.collect(ids.map((id) => findUser(id)));
161
+ // Option<User[]> - None if ANY lookup fails
162
+ ```
163
+
164
+ ### 3. Transformation Pipelines
165
+
166
+ **Habit:** Instead of imperatively checking for success/failure, use combinators to build a pipeline.
167
+
168
+ - **Transform.** `.map()` / `.mapErr()` - "If I have a value, change it."
169
+ - **Chain.** `.andThen()` - "If this succeeds, try this next risky thing."
170
+ - **Extract.** `.unwrapOr()` / `.match()` - "Get me out of the wrapper."
171
+
172
+ _This pattern is visible in the `Result` and `Option` examples above._
173
+
174
+ ### 4. Exhaustive Matching
175
+
176
+ **Habit:** Always handle every case of a discriminated union.
177
+
178
+ - Use `switch` statements with `assertNever(x)` in the `default` case.
179
+ - This ensures that if you add a new error type or state later, the compiler forces you to update every call site.
180
+
181
+ **Feature: Pattern Matching**
182
+ Ensure all cases are handled in discriminated unions.
183
+
184
+ ```typescript
185
+ import { assertNever, matchKind, matchType } from "rustlike";
186
+
187
+ type Shape = { kind: "circle"; radius: number } | { kind: "rect"; w: number; h: number };
188
+
189
+ // Using matchKind helper
190
+ const area = matchKind(shape, {
191
+ circle: (s) => Math.PI * s.radius ** 2,
192
+ rect: (s) => s.w * s.h,
193
+ });
194
+
195
+ // Using assertNever in switch
196
+ function getArea(s: Shape): number {
197
+ switch (s.kind) {
198
+ case "circle":
199
+ return Math.PI * s.radius ** 2;
200
+ case "rect":
201
+ return s.w * s.h;
202
+ default:
203
+ return assertNever(s); // Compile error if case missing
204
+ }
205
+ }
206
+ ```
207
+
208
+ ### 5. Parse, Don't Validate (Branded Types)
209
+
210
+ **Habit:** Make invalid states unrepresentable.
211
+ Implement Rust's "make invalid states unrepresentable" pattern using branded types with validation. This forces validation at construction time. You cannot create an invalid value.
212
+
213
+ **Feature: Branded Types**
214
+ Nominal typing for type-safe IDs and domain values.
215
+
216
+ ```typescript
217
+ import { Brand, brand } from "rustlike";
218
+
219
+ type UserId = Brand<number, "UserId">;
220
+ type OrderId = Brand<number, "OrderId">;
221
+
222
+ const UserId = brand<number, "UserId">();
223
+ const OrderId = brand<number, "OrderId">();
224
+
225
+ const userId = UserId(42);
226
+ const orderId = OrderId(42);
227
+
228
+ // These are now incompatible even though both are numbers
229
+ // orderId = userId; // Type error!
230
+ ```
231
+
232
+ **Feature: Validation Helpers**
233
+
234
+ Pattern 1: Manual (using Brand + Result directly)
235
+
236
+ ```typescript
237
+ import { Brand, Result, Ok, Err } from "rustlike";
238
+
239
+ // Define the branded type
240
+ type EmailAddress = Brand<string, "Email">;
241
+
242
+ // Create a namespace with validation
243
+ const EmailAddress = {
244
+ parse: (input: string): Result<EmailAddress, string> => {
245
+ if (input.includes("@")) {
246
+ return Ok(input as EmailAddress);
247
+ }
248
+ return Err("Invalid email");
249
+ },
250
+
251
+ unsafe: (input: string): EmailAddress => input as EmailAddress,
252
+ };
253
+
254
+ // Usage - validation is mandatory
255
+ const email = EmailAddress.parse(userInput);
256
+
257
+ email.match({
258
+ ok: (validEmail) => sendWelcome(validEmail),
259
+ err: (msg) => console.error(msg),
260
+ });
261
+
262
+ // This function can ONLY accept validated emails
263
+ function sendWelcome(email: EmailAddress) {
264
+ // No need to validate - type system guarantees it
265
+ }
266
+ ```
267
+
268
+ Pattern 2: Using the `newtype()` helper
269
+
270
+ ```typescript
271
+ import { newtype } from "rustlike";
272
+
273
+ // Single line definition
274
+ const EmailAddress = newtype<string, "Email">((s) => s.includes("@"), "Invalid email");
275
+
276
+ // Same usage, same type safety
277
+ const email = EmailAddress.parse(userInput);
278
+
279
+ // More examples
280
+ const PositiveNumber = newtype<number, "Positive">(
281
+ (n) => n > 0,
282
+ (n) => `Expected positive, got ${n}`,
283
+ );
284
+
285
+ const NonEmptyString = newtype<string, "NonEmpty">((s) => s.length > 0, "String cannot be empty");
286
+
287
+ const StrongPassword = newtype<string, "StrongPassword">(
288
+ (s) => s.length >= 8 && /[A-Z]/.test(s) && /[0-9]/.test(s),
289
+ "Password must be 8+ chars with uppercase and number",
290
+ );
291
+ ```
292
+
293
+ ## Additional Features
294
+
295
+ ### Immutability Helpers
296
+
297
+ ```typescript
298
+ import { DeepReadonly, NonEmptyArray, isNonEmpty, head } from "rustlike";
299
+
300
+ // Deep immutability
301
+ type Config = DeepReadonly<{
302
+ server: { host: string; port: number };
303
+ options: string[];
304
+ }>;
305
+
306
+ // Non-empty arrays
307
+ const items: NonEmptyArray<string> = ["a", "b", "c"];
308
+ const first = head(items); // Guaranteed to exist
309
+
310
+ // Type guard for arrays
311
+ if (isNonEmpty(arr)) {
312
+ const first = head(arr); // Safe!
313
+ }
314
+ ```
315
+
316
+ ### Interop Helpers
317
+
318
+ Wrappers for integrating with third-party libraries and code that throws or returns nullable values.
319
+
320
+ ```typescript
321
+ import { tryCatch, tryAsync, safeCall, safeTry } from "rustlike";
322
+
323
+ // One-off sync try/catch
324
+ const parsed = tryCatch(() => JSON.parse(userInput));
325
+ // Result<unknown, unknown>
326
+
327
+ // One-off async try/catch (e.g., axios, fetch)
328
+ const response = await tryAsync(() => axios.get<User[]>("/api/users"));
329
+ // Result<AxiosResponse<User[]>, AxiosError>
330
+
331
+ // Create reusable wrapper for nullable functions
332
+ const safeFind = safeCall((id: number) => users.find((u) => u.id === id));
333
+ const user = safeFind(42);
334
+ // Option<User>
335
+
336
+ // Create reusable wrapper for throwing functions
337
+ const safeJsonParse = safeTry(JSON.parse);
338
+ const data = safeJsonParse(input);
339
+ // Result<unknown, unknown>
340
+ ```
341
+
342
+ For nullable values, use `Option.from`:
343
+
344
+ ```typescript
345
+ const user = Option.from(map.get("user")); // Option<User>
346
+ const item = Option.from(arr.find((x) => x.id)); // Option<Item>
347
+ ```
348
+
349
+ ### Lazy Iterators (`Iter<T>`, `AsyncIter<T>`)
350
+
351
+ Rust-like lazy iterators for composable, memory-efficient data processing.
352
+
353
+ ```typescript
354
+ import { iter, iterLinesSync, asyncIterLines, Iter, AsyncIter, Ok, Err } from "rustlike";
355
+
356
+ // Basic pipeline
357
+ const doubled = iter([1, 2, 3, 4, 5])
358
+ .filter((x) => x % 2 === 0)
359
+ .map((x) => x * 2)
360
+ .collect(); // [4, 8]
361
+
362
+ // Lazy evaluation - nothing executes until collect()
363
+ const pipeline = iter(hugeArray).filter(expensive).map(transform).take(10); // Only processes first 10 matches
364
+
365
+ const results = pipeline.collect();
366
+
367
+ // File reading with Result error handling
368
+ iterLinesSync("data.csv").match({
369
+ ok: (lines) => {
370
+ const parsed = lines
371
+ .skip(1) // skip header
372
+ .map((line) => line.split(","))
373
+ .filter((cols) => cols.length >= 3)
374
+ .collect();
375
+ },
376
+ err: (e) => console.error(e),
377
+ });
378
+
379
+ // Async streaming for large files
380
+ const result = await asyncIterLines("huge.log");
381
+ result.match({
382
+ ok: async (lines) => {
383
+ const errors = await lines
384
+ .filter((line) => line.includes("ERROR"))
385
+ .take(100)
386
+ .collect();
387
+ },
388
+ err: (e) => console.error(e),
389
+ });
390
+
391
+ // Pagination pattern with flatten()
392
+ const allItems = iter(pages)
393
+ .map((page) => page.items)
394
+ .flatten()
395
+ .filter((item) => item.active)
396
+ .collect();
397
+
398
+ // Error recovery with Result + collectResult()
399
+ const results = iter(inputs)
400
+ .map((input) => (validate(input) ? Ok(parse(input)) : Err("invalid")))
401
+ .collectResult();
402
+
403
+ // Combining data sources with chain()
404
+ const combined = iter(localData).chain(remoteData).map(normalize).collect();
405
+
406
+ // Zip for parallel iteration
407
+ const pairs = iter(keys).zip(values).collect(); // [[key1, val1], [key2, val2], ...]
408
+
409
+ // Enumerate for indices
410
+ const numbered = iter(lines)
411
+ .enumerate()
412
+ .map(([i, line]) => `${i + 1}: ${line}`)
413
+ .collect();
414
+
415
+ // Peek for look-ahead (useful for multi-line parsing)
416
+ const iterator = iter(logLines).peekable();
417
+ while (iterator.peek().isSome()) {
418
+ const line = iterator.next().value;
419
+ // Check if next line is a continuation
420
+ if (
421
+ iterator
422
+ .peek()
423
+ .map((l) => l.startsWith(" "))
424
+ .unwrapOr(false)
425
+ ) {
426
+ // Handle multi-line entry
427
+ }
428
+ }
429
+ ```
430
+
431
+ **Key Methods:**
432
+
433
+ | Method | Description |
434
+ | ------------------- | ------------------------------------------------------------ |
435
+ | `map(fn)` | Transform each element |
436
+ | `filter(pred)` | Keep matching elements |
437
+ | `fold(init, fn)` | Reduce to single value |
438
+ | `tryFold(init, fn)` | Fold with early exit on `Err` |
439
+ | `tryMap(fn)` | Map with fallible function |
440
+ | `take(n)` | Take first n elements |
441
+ | `skip(n)` | Skip first n elements |
442
+ | `enumerate()` | Add indices: `[index, value]` |
443
+ | `zip(other)` | Pair with another iterator |
444
+ | `chain(other)` | Concatenate iterators |
445
+ | `flatten()` | Flatten nested iterables |
446
+ | `peekable()` | Adapter that enables look-ahead via `peek()` |
447
+ | `collect()` | Gather into array |
448
+ | `collectResult()` | Rust-like `Result` collection (short-circuit on first `Err`) |
449
+
450
+ **Factory Functions:**
451
+
452
+ ```typescript
453
+ // Sync iterators
454
+ iter([1, 2, 3]); // From iterable
455
+ iterFromArray(arr); // From array
456
+ iterFromGenerator(gen); // From generator function
457
+ iterLinesSync(filepath); // From file (returns Result)
458
+ Iter.range(0, 10); // Range of numbers
459
+ Iter.repeat(value, n); // Repeat value n times
460
+ Iter.once(value); // Single value
461
+ Iter.empty(); // Empty iterator
462
+
463
+ // Async iterators
464
+ asyncIter(asyncIterable); // From async iterable
465
+ asyncIterFromArray(arr); // From array (async)
466
+ asyncIterFromGenerator(gen); // From async generator
467
+ asyncIterLines(filepath); // Stream file (returns Promise<Result>)
468
+ AsyncIter.range(0, 10); // Async range
469
+ ```
470
+
471
+ ### Async Chaining (`AsyncResult`)
472
+
473
+ A powerful wrapper around `Promise<Result<T, E>>` to enable method chaining on async operations.
474
+
475
+ ```typescript
476
+ import { AsyncResult, Ok, Err } from "rustlike";
477
+
478
+ // Instead of:
479
+ // const res = await doAsyncThing();
480
+ // if (res.isOk()) { ... }
481
+
482
+ // You can chain:
483
+ const user = await AsyncResult.fromPromise(fetchUser(id))
484
+ .map((user) => user.name)
485
+ .andThen((name) => AsyncResult.fromPromise(validateName(name)))
486
+ .unwrapOr("guest");
487
+ ```
488
+
489
+ ## Design Philosophy
490
+
491
+ ### What we do _not_ implement:
492
+
493
+ - **Traits.** TypeScript has interfaces; we use those.
494
+ - **Borrow Checker.** Impossible to implement in TS without a compiler plugin.
495
+ - **Panic/Unwind.** We use standard Exceptions for panics (`expect`, `unwrap` failures).
496
+ - **Niche Stdlib Functions.** We don't need `Vec::dedup_by_key` or `BTreeMap`. TypeScript's Arrays and Objects are sufficient.
497
+ - **Operator Overloading.** TypeScript doesn't support it.
498
+
499
+ ### Benefits
500
+
501
+ **Errors become visible in types.** A function returning `Result<User, ApiError>` tells you it can fail. A function returning `User` that silently throws? You'd never know without reading the implementation.
502
+
503
+ **No null surprises.** `Option<T>` forces you to handle the absent case. No more `Cannot read property 'x' of undefined` at runtime.
504
+
505
+ **Exhaustive handling.** Add a variant to a union, and the compiler tells you everywhere you forgot to handle it.
506
+
507
+ **Immutability by default.** Easier to reason about. No mutations hiding in called functions.
508
+
509
+ **Self-documenting code.** Types encode behavior that would otherwise live in comments or nowhere.
510
+
511
+ ### Tradeoffs
512
+
513
+ This approach has real costs. Be aware of them.
514
+
515
+ **Non-idiomatic.** This is not how TypeScript is typically written. Other developers may find it unfamiliar or harder to read. The patterns aren't in most TS style guides.
516
+
517
+ **Verbosity.** Compare:
518
+
519
+ ```typescript
520
+ // Typical TS (minimal, non-exhaustive, common in apps)
521
+ const user = getUser(id); // User | undefined
522
+ const name = user?.name ?? "guest"; // fine for many teams
523
+
524
+ // Rust-like TS (more explicit / verbose on purpose)
525
+ import { Option, Ok, Err } from "rustlike";
526
+
527
+ // Option chaining example
528
+ const name2 = Option.from(getUser(id))
529
+ .map((u) => u.name)
530
+ .okOr("missing user name") // document failure path
531
+ .andThen((n) => (n.length > 0 ? Ok(n) : Err("empty name")))
532
+ .unwrapOr("guest"); // explicit fallback
533
+
534
+ // match-based example
535
+ const name3 = Option.from(getUser(id))
536
+ .match({
537
+ some: (u) => (u.name.length > 0 ? Ok(u.name) : Err("empty name")),
538
+ none: () => Err("missing user name"),
539
+ })
540
+ .unwrapOr("guest");
541
+ ```
542
+
543
+ Method chaining helps, but it's still more explicit than implicit null checks.
544
+
545
+ **Runtime overhead.** Every `Ok(value)` creates an object with methods. It's small, but not zero.
546
+
547
+ **Spreading loses methods.** `{ ...Ok(5) }` becomes a plain object without methods. Same with `Object.assign`. This is similar to Rust. You can't destructure and expect impl methods to follow.
548
+
549
+ **Ecosystem friction.** Most libraries throw exceptions and return `null`. You'll wrap at boundaries:
550
+
551
+ ```typescript
552
+ const result = Result.fromPromise(fetch(url));
553
+ const user = Option.from(localStorage.getItem("user"));
554
+ ```
555
+
556
+ **TypeScript isn't Rust.** The type system lacks higher-kinded types, true exhaustiveness checking, and default immutability. You're simulating Rust semantics in a system not built for them.
557
+
558
+ ### When to use this
559
+
560
+ - Personal projects or learning exercises
561
+ - Codebases where correctness matters more than convention
562
+ - Isolated modules where the pattern stays contained
563
+ - Preparing to learn Rust
564
+
565
+ ### When not to
566
+
567
+ - Large teams unfamiliar with the patterns
568
+ - Heavy third-party library integration (constant wrapping)
569
+ - Performance-critical inner loops
570
+ - When shipping speed matters more than style
571
+
572
+ ## API Summary
573
+
574
+ ### Result<T, E>
575
+
576
+ | Method / Function | Description |
577
+ | --------------------------- | -------------------------------------------------------- |
578
+ | `Ok(value)` | Create success result |
579
+ | `Err(error)` | Create error result |
580
+ | `.map(fn)` | Transform success value |
581
+ | `.mapOr(default, fn)` | Transform success value, or return default if Err |
582
+ | `.mapOrElse(defaultFn, fn)` | Transform success value, or compute default from error |
583
+ | `.mapErr(fn)` | Transform error value |
584
+ | `.andThen(fn)` | Chain Result-returning functions |
585
+ | `.and(other)` | Return `other` if Ok, otherwise propagate Err |
586
+ | `.or(other)` | Return this if Ok, otherwise return `other` |
587
+ | `.orElse(fn)` | Handle error with fallback Result |
588
+ | `.contains(value)` | Check if Ok contains a specific value (strict equality) |
589
+ | `.containsErr(error)` | Check if Err contains a specific error (strict equality) |
590
+ | `.match({ ok, err })` | Pattern match on result |
591
+ | `.unwrap()` | Get value or throw |
592
+ | `.unwrapOr(default)` | Get value or default |
593
+ | `.unwrapOrElse(fn)` | Get value or compute default |
594
+ | `.expect(message)` | Get value or throw with custom message |
595
+ | `.unwrapErr()` | Get the error value or throw |
596
+ | `.expectErr(message)` | Get the error value or throw with custom message |
597
+ | `.toOption()` | Convert Ok to Some, Err to None |
598
+ | `.err()` | Convert Err to Some, Ok to None |
599
+ | `.isOk()` / `.isErr()` | Type guards (method form) |
600
+ | `.flatten()` | Flatten `Result<Result<T, E>, E>` to `Result<T, E>` |
601
+ | `.inspect(fn)` | Run effect if Ok (pass-through) |
602
+ | `.inspectErr(fn)` | Run effect if Err (pass-through) |
603
+ | `isOk(result)` | Standalone type guard function |
604
+ | `isErr(result)` | Standalone type guard function |
605
+ | `Result.fromThrowable(fn)` | Convert throwing function to Result |
606
+ | `Result.fromPromise(p)` | Convert Promise to Result |
607
+ | `Result.all(results)` | Combine array of Results, short-circuit on first Err |
608
+ | `Result.collect(results)` | Alias for Result.all() |
609
+ | `Result.transpose(res)` | Swap `Result<Option<T>, E>` → `Option<Result<T, E>>` |
610
+
611
+ ### Option<T>
612
+
613
+ | Method / Function | Description |
614
+ | --------------------------- | --------------------------------------------------------- |
615
+ | `Some(value)` | Create Some option |
616
+ | `None` | The None value |
617
+ | `Option.from(val)` | Create from nullable |
618
+ | `.map(fn)` | Transform value if present |
619
+ | `.mapOr(default, fn)` | Transform value if present, or return default |
620
+ | `.mapOrElse(defaultFn, fn)` | Transform value if present, or compute default |
621
+ | `.andThen(fn)` | Chain Option-returning functions |
622
+ | `.and(other)` | Return `other` if Some, otherwise None |
623
+ | `.or(other)` | Return this if Some, else other |
624
+ | `.orElse(fn)` | Return this if Some, else compute |
625
+ | `.xor(other)` | Return Some if exactly one is Some, else None |
626
+ | `.filter(pred)` | Keep only if predicate passes |
627
+ | `.contains(value)` | Check if Some contains a specific value (strict equality) |
628
+ | `.match({ some, none })` | Pattern match on option |
629
+ | `.unwrap()` | Get value or throw |
630
+ | `.unwrapOr(default)` | Get value or default |
631
+ | `.unwrapOrElse(fn)` | Get value or compute default |
632
+ | `.expect(message)` | Get value or throw with custom message |
633
+ | `.okOr(error)` | Convert to Result |
634
+ | `.isSome()` / `.isNone()` | Type guards (method form) |
635
+ | `.zip(other)` | Combine two options into `Option<[T, U]>` |
636
+ | `.flatten()` | Flatten `Option<Option<T>>` to `Option<T>` |
637
+ | `.inspect(fn)` | Run effect if Some (pass-through) |
638
+ | `isSome(option)` | Standalone type guard function |
639
+ | `isNone(option)` | Standalone type guard function |
640
+ | `Option.all(options)` | Combine array of Options, None if any is None |
641
+ | `Option.collect(options)` | Alias for Option.all() |
642
+ | `Option.transpose(opt)` | Swap `Option<Result<T, E>>` → `Result<Option<T>, E>` |
643
+
644
+ ### AsyncResult<T, E>
645
+
646
+ A chainable wrapper around `Promise<Result<T, E>>`.
647
+
648
+ | Method / Function | Description |
649
+ | ------------------------------- | -------------------------------------- |
650
+ | `AsyncResult.ok(val)` | Create async success |
651
+ | `AsyncResult.err(err)` | Create async error |
652
+ | `AsyncResult.fromPromise(p)` | Create from `Promise<Result>` |
653
+ | `AsyncResult.fromThrowable(fn)` | Create from async throwing function |
654
+ | `.map(fn)` | Async transform success value |
655
+ | `.mapErr(fn)` | Async transform error value |
656
+ | `.andThen(fn)` | Async chain Result-returning functions |
657
+ | `.orElse(fn)` | Async handle error |
658
+ | `.match(handlers)` | Async pattern match |
659
+ | `.unwrap()` | Async get value or throw |
660
+ | `.unwrapOr(default)` | Async get value or default |
661
+ | `.unwrapOrElse(fn)` | Async get value or compute default |
662
+ | `.inspect(fn)` | Async inspect Ok value |
663
+ | `.inspectErr(fn)` | Async inspect Err value |
664
+ | `.toPromise()` | Convert back to `Promise<Result>` |
665
+ | `await` | `AsyncResult` is `PromiseLike` |
666
+
667
+ ### Pattern Matching
668
+
669
+ | Function | Description |
670
+ | -------------------------------------- | -------------------------------------------------------------------- |
671
+ | `assertNever(x, msg?)` | Exhaustiveness check in switch default |
672
+ | `match(value, discriminant, handlers)` | Generic discriminated union matcher on any discriminant key |
673
+ | `matchKind(value, handlers)` | Match on `kind` discriminant (shorthand for `match(v, 'kind', ...)`) |
674
+ | `matchType(value, handlers)` | Match on `type` discriminant (shorthand for `match(v, 'type', ...)`) |
675
+
676
+ All matchers support a catch-all `_` handler for partial matching:
677
+
678
+ ```typescript
679
+ const isCircle = matchKind(shape, {
680
+ circle: () => true,
681
+ _: () => false, // catch-all for all other variants
682
+ });
683
+ ```
684
+
685
+ ### Types & Utilities
686
+
687
+ | Type / Function | Description |
688
+ | ---------------------- | ------------------------------------------------ |
689
+ | `DeepReadonly<T>` | Recursive readonly |
690
+ | `ReadonlyPick<T, K>` | Pick keys and make them readonly |
691
+ | `Brand<T, B>` | Branded/nominal type (newtype pattern) |
692
+ | `brand<T, B>()` | Create a brand constructor |
693
+ | `newtype<T, B, E>()` | Create validated newtype (parse, don't validate) |
694
+ | `NonEmptyArray<T>` | Array with at least one element |
695
+ | `isNonEmpty(arr)` | Type guard for NonEmptyArray |
696
+ | `nonEmpty(a, ...rest)` | Create NonEmptyArray |
697
+ | `head(arr)` | Get first element (safe on NonEmptyArray) |
698
+
699
+ ### Interop Helpers
700
+
701
+ | Function | Description |
702
+ | -------------- | ----------------------------------------- |
703
+ | `tryCatch(fn)` | Wrap sync throwing function → Result |
704
+ | `tryAsync(fn)` | Wrap async function → Result |
705
+ | `safeCall(fn)` | Create reusable nullable → Option wrapper |
706
+ | `safeTry(fn)` | Create reusable throwing → Result wrapper |
707
+
708
+ ### Iter\<T\>
709
+
710
+ Lazy synchronous iterator.
711
+
712
+ | Method / Function | Description |
713
+ | ------------------------------- | ----------------------------------------------- |
714
+ | `iter(source)` | Create from iterable |
715
+ | `iterFromArray(arr)` | Create from array |
716
+ | `iterFromGenerator(fn)` | Create from generator function |
717
+ | `iterLinesSync(path)` | Create from file (returns `Result`) |
718
+ | `Iter.range(start, end, step?)` | Range of numbers |
719
+ | `Iter.repeat(value, n)` | Repeat value n times |
720
+ | `Iter.once(value)` | Single value |
721
+ | `Iter.empty()` | Empty iterator |
722
+ | `Iter.sum(iter)` | Sum all numbers in an iterator |
723
+ | `Iter.product(iter)` | Multiply all numbers in an iterator |
724
+ | `Iter.min(iter)` | Minimum value, or `None` for empty iterators |
725
+ | `Iter.max(iter)` | Maximum value, or `None` for empty iterators |
726
+ | `.map(fn)` | Transform each element |
727
+ | `.filter(pred)` | Keep matching elements |
728
+ | `.flatMap(fn)` | Map each element to an iterable and flatten |
729
+ | `.inspect(fn)` | Run side effect on each element (pass-through) |
730
+ | `.find(pred)` | First element matching predicate → `Option<T>` |
731
+ | `.findMap(fn)` | Find and transform in one step → `Option<U>` |
732
+ | `.any(pred)` | `true` if any element matches predicate |
733
+ | `.all(pred)` | `true` if all elements match predicate |
734
+ | `.position(pred)` | Index of first match → `Option<number>` |
735
+ | `.fold(init, fn)` | Reduce to single value |
736
+ | `.reduce(fn)` | Reduce without initial value → `Option<T>` |
737
+ | `.tryFold(init, fn)` | Fold with early exit on `Err` |
738
+ | `.tryMap(fn)` | Map with fallible function |
739
+ | `.count()` | Count elements (consumes iterator) |
740
+ | `.last()` | Get last element → `Option<T>` |
741
+ | `.nth(n)` | Get nth element → `Option<T>` |
742
+ | `.partition(pred)` | Split into `[matching[], rest[]]` |
743
+ | `.take(n)` | Take first n elements |
744
+ | `.skip(n)` | Skip first n elements |
745
+ | `.stepBy(step)` | Yield every nth element |
746
+ | `.enumerate()` | Add indices: `[index, value]` |
747
+ | `.zip(other)` | Pair with another iterator |
748
+ | `.chain(other)` | Concatenate iterators |
749
+ | `.flatten()` | Flatten nested iterables |
750
+ | `.peekable()` | Enable look-ahead via `peek()` |
751
+ | `.collect()` | Gather into array |
752
+ | `.collectResult()` | Collect `Result`s, short-circuit on first `Err` |
753
+
754
+ ### AsyncIter\<T\>
755
+
756
+ Lazy asynchronous iterator. All callbacks accept sync or async functions.
757
+
758
+ | Method / Function | Description |
759
+ | ------------------------------------ | ------------------------------------------------------- |
760
+ | `asyncIter(source)` | Create from async iterable |
761
+ | `asyncIterFromArray(arr)` | Create from array |
762
+ | `asyncIterFromIterable(iter)` | Create from sync iterable |
763
+ | `asyncIterFromGenerator(fn)` | Create from async generator function |
764
+ | `asyncIterLines(path)` | Stream file lines (returns `Promise<Result>`) |
765
+ | `AsyncIter.range(start, end, step?)` | Async range of numbers |
766
+ | `AsyncIter.repeat(value, n)` | Repeat value n times |
767
+ | `AsyncIter.once(value)` | Single value |
768
+ | `AsyncIter.empty()` | Empty iterator |
769
+ | `AsyncIter.sum(iter)` | Sum all numbers in an async iterator |
770
+ | `AsyncIter.product(iter)` | Multiply all numbers in an async iterator |
771
+ | `AsyncIter.min(iter)` | Minimum value, or `None` for empty iterators |
772
+ | `AsyncIter.max(iter)` | Maximum value, or `None` for empty iterators |
773
+ | `.map(fn)` | Transform each element (sync or async fn) |
774
+ | `.filter(pred)` | Keep matching elements (sync or async pred) |
775
+ | `.flatMap(fn)` | Map each element to an iterable and flatten |
776
+ | `.inspect(fn)` | Run side effect on each element (pass-through) |
777
+ | `.find(pred)` | First element matching predicate → `Promise<Option<T>>` |
778
+ | `.findMap(fn)` | Find and transform in one step → `Promise<Option<U>>` |
779
+ | `.any(pred)` | `true` if any element matches predicate |
780
+ | `.all(pred)` | `true` if all elements match predicate |
781
+ | `.position(pred)` | Index of first match → `Promise<Option<number>>` |
782
+ | `.fold(init, fn)` | Reduce to single value |
783
+ | `.reduce(fn)` | Reduce without initial value → `Promise<Option<T>>` |
784
+ | `.tryFold(init, fn)` | Fold with early exit on `Err` |
785
+ | `.tryMap(fn)` | Map with fallible function |
786
+ | `.count()` | Count elements (consumes iterator) |
787
+ | `.last()` | Get last element → `Promise<Option<T>>` |
788
+ | `.nth(n)` | Get nth element → `Promise<Option<T>>` |
789
+ | `.partition(pred)` | Split into `Promise<[matching[], rest[]]>` |
790
+ | `.take(n)` | Take first n elements |
791
+ | `.skip(n)` | Skip first n elements |
792
+ | `.stepBy(step)` | Yield every nth element |
793
+ | `.enumerate()` | Add indices: `[index, value]` |
794
+ | `.zip(other)` | Pair with another async iterator |
795
+ | `.chain(other)` | Concatenate iterators |
796
+ | `.flatten()` | Flatten nested iterables |
797
+ | `.peekable()` | Enable look-ahead via `peek()` |
798
+ | `.collect()` | Gather into array |
799
+ | `.collectResult()` | Collect `Result`s, short-circuit on first `Err` |
800
+
801
+ ## ESLint Plugin
802
+
803
+ The library includes an ESLint plugin to help enforce Rust-like patterns in your codebase.
804
+
805
+ ### Installation
806
+
807
+ ```bash
808
+ # Install the plugin (once published)
809
+ pnpm add -D @dangayle/eslint-plugin-rustlike
810
+ ```
811
+
812
+ ### Configuration
813
+
814
+ Add to your ESLint config (`eslint.config.js`):
815
+
816
+ ```javascript
817
+ import rustlikePlugin from "@dangayle/eslint-plugin-rustlike";
818
+
819
+ export default [
820
+ {
821
+ plugins: {
822
+ rustlike: rustlikePlugin,
823
+ },
824
+ rules: {
825
+ // Recommended rules (low-noise)
826
+ "rustlike/no-object-spread-on-adt": "warn",
827
+ "rustlike/prefer-match": "warn",
828
+
829
+ // Strict rules (opt-in)
830
+ "rustlike/no-unwrap": "error",
831
+ "rustlike/no-throw-in-result-returning-function": "error",
832
+ },
833
+ },
834
+ ];
835
+ ```
836
+
837
+ ### Rules
838
+
839
+ #### Recommended (low-noise)
840
+
841
+ | Rule | Description |
842
+ | ------------------------- | ----------------------------------------------------------------------- |
843
+ | `no-object-spread-on-adt` | Warns when spreading `Ok/Err/Some/None` objects, which strips methods |
844
+ | `prefer-match` | Suggests `.match()` for simple `if/else` on `Result/Option` type guards |
845
+
846
+ #### Strict (opt-in)
847
+
848
+ | Rule | Description |
849
+ | --------------------------------------- | ------------------------------------------------------------------------------ |
850
+ | `no-unwrap` | Bans `.unwrap()`, `.unwrapErr()`, `.expect()` - forces explicit error handling |
851
+ | `no-throw-in-result-returning-function` | Disallows `throw` in functions returning `Result` - use `Err()` instead |
852
+
853
+ ### Private vs Public Linting
854
+
855
+ This repo uses two ESLint configurations:
856
+
857
+ - **Internal (src/)**: Heavy-handed TypeScript correctness rules (no `any`, no assertions, etc.) - idiomatic, strict TypeScript.
858
+ - **Public (plugin)**: Rust-like pattern enforcement for library consumers - enforces the mental model.
859
+
860
+ The examples use the public plugin to demonstrate real-world usage.
861
+
862
+ ## License
863
+
864
+ MIT