rsult 1.3.0 → 2.0.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.
@@ -0,0 +1,467 @@
1
+ import { Option, OptionSome, OptionNone, Some, None, FlattenOption } from './option';
2
+
3
+ // ============================================================================
4
+ // Type Utilities
5
+ // ============================================================================
6
+
7
+ /** Type for functions that may be sync or async */
8
+ type MaybeAsync<T> = T | Promise<T>;
9
+
10
+ /** Type for Option that may be wrapped in Promise */
11
+ type MaybeAsyncOption<T> = MaybeAsync<Option<T>>;
12
+
13
+ // ============================================================================
14
+ // OptionAsync Class
15
+ // ============================================================================
16
+
17
+ /**
18
+ * A wrapper around `Promise<Option<T>>` that enables fluent async chains.
19
+ *
20
+ * `OptionAsync` allows you to chain operations on async Options without
21
+ * needing to await at each step. All transformations are lazily composed
22
+ * and only executed when you await the final result.
23
+ *
24
+ * @typeParam T - The contained value type
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const result = await OptionAsync.fromPromise(fetchUser(id))
29
+ * .map(user => user.profile)
30
+ * .andThen(profile => fetchAvatar(profile.avatarId))
31
+ * .unwrapOr(defaultAvatar);
32
+ * ```
33
+ */
34
+ export class OptionAsync<T> implements PromiseLike<Option<T>> {
35
+ private constructor(private readonly promise: Promise<Option<T>>) {}
36
+
37
+ // ========================================================================
38
+ // Constructors
39
+ // ========================================================================
40
+
41
+ /**
42
+ * Creates an OptionAsync from an existing Promise<Option<T>>.
43
+ */
44
+ static fromOption<T>(option: MaybeAsyncOption<T>): OptionAsync<T> {
45
+ return new OptionAsync(Promise.resolve(option));
46
+ }
47
+
48
+ /**
49
+ * Creates an OptionAsync containing the value.
50
+ */
51
+ static some<T>(value: T): OptionAsync<T> {
52
+ return new OptionAsync(Promise.resolve(Some(value)));
53
+ }
54
+
55
+ /**
56
+ * Creates an empty OptionAsync.
57
+ */
58
+ static none<T = never>(): OptionAsync<T> {
59
+ return new OptionAsync(Promise.resolve(None<T>()));
60
+ }
61
+
62
+ /**
63
+ * Wraps a Promise, converting resolution to Some and rejection to None.
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const result = await OptionAsync.fromPromise(fetch('/api/data'));
68
+ * ```
69
+ */
70
+ static fromPromise<T>(promise: Promise<T>): OptionAsync<T> {
71
+ return new OptionAsync(
72
+ promise
73
+ .then((value) => Some(value))
74
+ .catch(() => None<T>())
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Creates an OptionAsync from a nullable value.
80
+ */
81
+ static fromNullable<T>(value: T | null | undefined): OptionAsync<NonNullable<T>> {
82
+ if (value === null || value === undefined) {
83
+ return OptionAsync.none();
84
+ }
85
+ return OptionAsync.some(value as NonNullable<T>);
86
+ }
87
+
88
+ /**
89
+ * Wraps a function that may throw, executing it and capturing the result.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * const result = await OptionAsync.try(async () => {
94
+ * const res = await fetch('/api');
95
+ * return res.json();
96
+ * });
97
+ * ```
98
+ */
99
+ static try<T>(fn: () => MaybeAsync<T>): OptionAsync<T> {
100
+ return new OptionAsync(
101
+ (async () => {
102
+ try {
103
+ const value = await fn();
104
+ return Some(value);
105
+ } catch {
106
+ return None<T>();
107
+ }
108
+ })()
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Combines multiple OptionAsync values into a single OptionAsync containing an array.
114
+ *
115
+ * Returns None if any option is None.
116
+ */
117
+ static all<T extends readonly OptionAsync<any>[]>(
118
+ options: T
119
+ ): OptionAsync<{ [K in keyof T]: T[K] extends OptionAsync<infer U> ? U : never }> {
120
+ return new OptionAsync(
121
+ (async () => {
122
+ const values: any[] = [];
123
+ for (const optionAsync of options) {
124
+ const option = await optionAsync;
125
+ if (option.is_none()) {
126
+ return None() as any;
127
+ }
128
+ values.push(option.value);
129
+ }
130
+ return Some(values) as any;
131
+ })()
132
+ );
133
+ }
134
+
135
+ // ========================================================================
136
+ // PromiseLike Implementation
137
+ // ========================================================================
138
+
139
+ /**
140
+ * Implements PromiseLike so OptionAsync can be awaited directly.
141
+ */
142
+ then<TResult1 = Option<T>, TResult2 = never>(
143
+ onfulfilled?: ((value: Option<T>) => TResult1 | PromiseLike<TResult1>) | null,
144
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
145
+ ): Promise<TResult1 | TResult2> {
146
+ return this.promise.then(onfulfilled, onrejected);
147
+ }
148
+
149
+ /**
150
+ * Returns the underlying Promise.
151
+ */
152
+ toPromise(): Promise<Option<T>> {
153
+ return this.promise;
154
+ }
155
+
156
+ // ========================================================================
157
+ // Type Checking
158
+ // ========================================================================
159
+
160
+ /**
161
+ * Async version of is_some(). Resolves to true if the option is Some.
162
+ */
163
+ async isSome(): Promise<boolean> {
164
+ const option = await this.promise;
165
+ return option.is_some();
166
+ }
167
+
168
+ /**
169
+ * Async version of is_none(). Resolves to true if the option is None.
170
+ */
171
+ async isNone(): Promise<boolean> {
172
+ const option = await this.promise;
173
+ return option.is_none();
174
+ }
175
+
176
+ // ========================================================================
177
+ // Transformations
178
+ // ========================================================================
179
+
180
+ /**
181
+ * Maps the Some value using a sync or async function.
182
+ */
183
+ map<U>(fn: (value: T) => MaybeAsync<U>): OptionAsync<U> {
184
+ return new OptionAsync(
185
+ this.promise.then(async (option) => {
186
+ if (option.is_some()) {
187
+ const newValue = await fn(option.value);
188
+ return Some(newValue);
189
+ }
190
+ return None<U>();
191
+ })
192
+ );
193
+ }
194
+
195
+ /**
196
+ * Maps the Some value, returning a default if None.
197
+ */
198
+ async mapOr<U>(defaultValue: U, fn: (value: T) => MaybeAsync<U>): Promise<U> {
199
+ const option = await this.promise;
200
+ if (option.is_some()) {
201
+ return fn(option.value);
202
+ }
203
+ return defaultValue;
204
+ }
205
+
206
+ /**
207
+ * Maps the Some value, computing a default if None.
208
+ */
209
+ async mapOrElse<U>(
210
+ defaultFn: () => MaybeAsync<U>,
211
+ fn: (value: T) => MaybeAsync<U>
212
+ ): Promise<U> {
213
+ const option = await this.promise;
214
+ if (option.is_some()) {
215
+ return fn(option.value);
216
+ }
217
+ return defaultFn();
218
+ }
219
+
220
+ // ========================================================================
221
+ // Chaining (Monadic Operations)
222
+ // ========================================================================
223
+
224
+ /**
225
+ * Chains a function that returns an Option or OptionAsync.
226
+ */
227
+ andThen<U>(
228
+ fn: (value: T) => MaybeAsync<Option<U>> | OptionAsync<U>
229
+ ): OptionAsync<U> {
230
+ return new OptionAsync(
231
+ this.promise.then(async (option) => {
232
+ if (option.is_some()) {
233
+ const next = fn(option.value);
234
+ if (next instanceof OptionAsync) {
235
+ return next.promise;
236
+ }
237
+ return next;
238
+ }
239
+ return None<U>();
240
+ })
241
+ );
242
+ }
243
+
244
+ /**
245
+ * Returns this if Some, otherwise evaluates the provided function.
246
+ */
247
+ orElse<U>(fn: () => MaybeAsync<Option<U>> | OptionAsync<U>): OptionAsync<T | U> {
248
+ return new OptionAsync(
249
+ this.promise.then(async (option) => {
250
+ if (option.is_none()) {
251
+ const next = fn();
252
+ if (next instanceof OptionAsync) {
253
+ return next.promise as Promise<Option<T | U>>;
254
+ }
255
+ return next as Option<T | U>;
256
+ }
257
+ return option as Option<T | U>;
258
+ })
259
+ );
260
+ }
261
+
262
+ /**
263
+ * Returns the provided OptionAsync if this is Some, otherwise returns None.
264
+ */
265
+ and<U>(other: OptionAsync<U>): OptionAsync<U> {
266
+ return new OptionAsync(
267
+ this.promise.then(async (option) => {
268
+ if (option.is_some()) {
269
+ return other.promise;
270
+ }
271
+ return None<U>();
272
+ })
273
+ );
274
+ }
275
+
276
+ /**
277
+ * Returns this if Some, otherwise returns the provided OptionAsync.
278
+ */
279
+ or<U>(other: OptionAsync<U>): OptionAsync<T | U> {
280
+ return new OptionAsync(
281
+ this.promise.then(async (option) => {
282
+ if (option.is_none()) {
283
+ return other.promise as Promise<Option<T | U>>;
284
+ }
285
+ return option as Option<T | U>;
286
+ })
287
+ );
288
+ }
289
+
290
+ // ========================================================================
291
+ // Filtering
292
+ // ========================================================================
293
+
294
+ /**
295
+ * Filters the Some value using a predicate.
296
+ */
297
+ filter(predicate: (value: T) => MaybeAsync<boolean>): OptionAsync<T> {
298
+ return new OptionAsync(
299
+ this.promise.then(async (option) => {
300
+ if (option.is_some()) {
301
+ const keep = await predicate(option.value);
302
+ if (keep) {
303
+ return option;
304
+ }
305
+ }
306
+ return None<T>();
307
+ })
308
+ );
309
+ }
310
+
311
+ // ========================================================================
312
+ // Unwrapping
313
+ // ========================================================================
314
+
315
+ /**
316
+ * Unwraps the Some value, or throws with the provided message.
317
+ */
318
+ async expect(msg: string): Promise<T> {
319
+ const option = await this.promise;
320
+ return option.expect(msg);
321
+ }
322
+
323
+ /**
324
+ * Unwraps the Some value, or throws.
325
+ */
326
+ async unwrap(): Promise<T> {
327
+ const option = await this.promise;
328
+ return option.unwrap();
329
+ }
330
+
331
+ /**
332
+ * Unwraps the Some value, or returns the provided default.
333
+ */
334
+ async unwrapOr(defaultValue: T): Promise<T> {
335
+ const option = await this.promise;
336
+ return option.unwrap_or(defaultValue);
337
+ }
338
+
339
+ /**
340
+ * Unwraps the Some value, or computes a default.
341
+ */
342
+ async unwrapOrElse(fn: () => MaybeAsync<T>): Promise<T> {
343
+ const option = await this.promise;
344
+ if (option.is_some()) {
345
+ return option.value;
346
+ }
347
+ return fn();
348
+ }
349
+
350
+ // ========================================================================
351
+ // Inspection
352
+ // ========================================================================
353
+
354
+ /**
355
+ * Calls the function with the Some value for side effects.
356
+ */
357
+ inspect(fn: (value: T) => void): OptionAsync<T> {
358
+ return new OptionAsync(
359
+ this.promise.then((option) => {
360
+ if (option.is_some()) {
361
+ fn(option.value);
362
+ }
363
+ return option;
364
+ })
365
+ );
366
+ }
367
+
368
+ // ========================================================================
369
+ // Conversion
370
+ // ========================================================================
371
+
372
+ /**
373
+ * Flattens an OptionAsync<Option<U>> into OptionAsync<U>.
374
+ */
375
+ flatten<U>(this: OptionAsync<Option<U>>): OptionAsync<U> {
376
+ return new OptionAsync(
377
+ this.promise.then((option) => {
378
+ if (option.is_some()) {
379
+ return option.value;
380
+ }
381
+ return None<U>();
382
+ })
383
+ );
384
+ }
385
+
386
+ /**
387
+ * Zips this OptionAsync with another.
388
+ */
389
+ zip<U>(other: OptionAsync<U>): OptionAsync<[T, U]> {
390
+ return new OptionAsync(
391
+ this.promise.then(async (option) => {
392
+ if (option.is_some()) {
393
+ const otherOption = await other.promise;
394
+ if (otherOption.is_some()) {
395
+ return Some([option.value, otherOption.value] as [T, U]);
396
+ }
397
+ }
398
+ return None<[T, U]>();
399
+ })
400
+ );
401
+ }
402
+
403
+ /**
404
+ * Zips with a function.
405
+ */
406
+ zipWith<U, R>(other: OptionAsync<U>, fn: (a: T, b: U) => MaybeAsync<R>): OptionAsync<R> {
407
+ return new OptionAsync(
408
+ this.promise.then(async (option) => {
409
+ if (option.is_some()) {
410
+ const otherOption = await other.promise;
411
+ if (otherOption.is_some()) {
412
+ const result = await fn(option.value, otherOption.value);
413
+ return Some(result);
414
+ }
415
+ }
416
+ return None<R>();
417
+ })
418
+ );
419
+ }
420
+
421
+ // ========================================================================
422
+ // Pattern Matching
423
+ // ========================================================================
424
+
425
+ /**
426
+ * Pattern matches on the option, calling the appropriate handler.
427
+ */
428
+ async match<R>(handlers: {
429
+ Some: (value: T) => MaybeAsync<R>;
430
+ None: () => MaybeAsync<R>;
431
+ }): Promise<R> {
432
+ const option = await this.promise;
433
+ if (option.is_some()) {
434
+ return handlers.Some(option.value);
435
+ }
436
+ return handlers.None();
437
+ }
438
+ }
439
+
440
+ // ============================================================================
441
+ // Extension: Add toAsync() method to Option
442
+ // ============================================================================
443
+
444
+ declare module './option' {
445
+ interface OptionSome<T> {
446
+ /**
447
+ * Lifts this Option into an OptionAsync.
448
+ */
449
+ toAsync(): OptionAsync<T>;
450
+ }
451
+
452
+ interface OptionNone<T> {
453
+ /**
454
+ * Lifts this Option into an OptionAsync.
455
+ */
456
+ toAsync(): OptionAsync<T>;
457
+ }
458
+ }
459
+
460
+ // Add toAsync to Option classes
461
+ OptionSome.prototype.toAsync = function <T>(this: OptionSome<T>): OptionAsync<T> {
462
+ return OptionAsync.fromOption(this);
463
+ };
464
+
465
+ OptionNone.prototype.toAsync = function <T>(this: OptionNone<T>): OptionAsync<T> {
466
+ return OptionAsync.fromOption(this);
467
+ };
@@ -429,4 +429,105 @@ describe('Option', () => {
429
429
  });
430
430
  });
431
431
  });
432
+
433
+ describe('New Rust-inspired Methods', () => {
434
+ describe('is_none_or', () => {
435
+ it('returns true if None', () => {
436
+ expect(None<number>().is_none_or(x => x > 5)).toBe(true);
437
+ });
438
+
439
+ it('returns true if Some and predicate is true', () => {
440
+ expect(Some(10).is_none_or(x => x > 5)).toBe(true);
441
+ });
442
+
443
+ it('returns false if Some and predicate is false', () => {
444
+ expect(Some(3).is_none_or(x => x > 5)).toBe(false);
445
+ });
446
+ });
447
+
448
+ describe('inspect', () => {
449
+ it('calls function with Some value', () => {
450
+ let inspected: number | null = null;
451
+ Some(42).inspect(x => { inspected = x; });
452
+ expect(inspected).toBe(42);
453
+ });
454
+
455
+ it('does not call function on None', () => {
456
+ let called = false;
457
+ None<number>().inspect(() => { called = true; });
458
+ expect(called).toBe(false);
459
+ });
460
+
461
+ it('returns self for chaining', () => {
462
+ const result = Some(5).inspect(() => {}).map(x => x * 2);
463
+ expect(result.unwrap()).toBe(10);
464
+ });
465
+ });
466
+
467
+ describe('ok_or', () => {
468
+ it('converts Some to Ok', () => {
469
+ const result = Some(42).ok_or('error');
470
+ expect(result._tag).toBe('Ok');
471
+ expect(result.value).toBe(42);
472
+ });
473
+
474
+ it('converts None to Err', () => {
475
+ const result = None<number>().ok_or('error');
476
+ expect(result._tag).toBe('Err');
477
+ expect(result.value).toBe('error');
478
+ });
479
+ });
480
+
481
+ describe('ok_or_else', () => {
482
+ it('converts Some to Ok without calling error fn', () => {
483
+ let called = false;
484
+ const result = Some(42).ok_or_else(() => { called = true; return 'error'; });
485
+ expect(result._tag).toBe('Ok');
486
+ expect(result.value).toBe(42);
487
+ expect(called).toBe(false);
488
+ });
489
+
490
+ it('converts None to Err by calling error fn', () => {
491
+ const result = None<number>().ok_or_else(() => 'computed error');
492
+ expect(result._tag).toBe('Err');
493
+ expect(result.value).toBe('computed error');
494
+ });
495
+ });
496
+
497
+ describe('unzip', () => {
498
+ it('unzips Some tuple into tuple of Somes', () => {
499
+ const [a, b] = Some([1, 'hello'] as [number, string]).unzip();
500
+ expect(a.unwrap()).toBe(1);
501
+ expect(b.unwrap()).toBe('hello');
502
+ });
503
+
504
+ it('unzips None into tuple of Nones', () => {
505
+ const [a, b] = None<[number, string]>().unzip();
506
+ expect(a.is_none()).toBe(true);
507
+ expect(b.is_none()).toBe(true);
508
+ });
509
+ });
510
+
511
+ describe('transpose', () => {
512
+ it('transposes Some(Ok(x)) to Ok(Some(x))', () => {
513
+ const optionOfResult = Some({ _tag: 'Ok' as const, value: 42 });
514
+ const result = optionOfResult.transpose();
515
+ expect(result._tag).toBe('Ok');
516
+ expect((result.value as any).unwrap()).toBe(42);
517
+ });
518
+
519
+ it('transposes Some(Err(e)) to Err(e)', () => {
520
+ const optionOfResult = Some({ _tag: 'Err' as const, value: 'error' });
521
+ const result = optionOfResult.transpose();
522
+ expect(result._tag).toBe('Err');
523
+ expect(result.value).toBe('error');
524
+ });
525
+
526
+ it('transposes None to Ok(None)', () => {
527
+ const result = None<{ _tag: 'Ok'; value: number }>().transpose();
528
+ expect(result._tag).toBe('Ok');
529
+ expect((result.value as any).is_none()).toBe(true);
530
+ });
531
+ });
532
+ });
432
533
  });