@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/LICENSE +22 -0
- package/README.md +864 -0
- package/dist/async-iter-1OXm1ncF.d.mts +468 -0
- package/dist/async-iter-1OXm1ncF.d.mts.map +1 -0
- package/dist/async-iter-3iu4aRtf.cjs +1129 -0
- package/dist/async-iter-3iu4aRtf.cjs.map +1 -0
- package/dist/async-iter-D7Pj6knS.d.cts +468 -0
- package/dist/async-iter-D7Pj6knS.d.cts.map +1 -0
- package/dist/async-iter-aLdg-qp2.mjs +1009 -0
- package/dist/async-iter-aLdg-qp2.mjs.map +1 -0
- package/dist/index.cjs +477 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +342 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +342 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +443 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node.cjs +118 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +46 -0
- package/dist/node.d.cts.map +1 -0
- package/dist/node.d.mts +46 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +87 -0
- package/dist/node.mjs.map +1 -0
- package/package.json +80 -0
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
|