ts-data-forge 14.4.0 → 14.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +6 -0
  2. package/dist/array/impl/array-utils-element-access.mjs +1 -1
  3. package/dist/array/impl/array-utils-reducing-value.mjs +3 -3
  4. package/dist/array/impl/array-utils-search.d.mts.map +1 -1
  5. package/dist/array/impl/array-utils-search.mjs +1 -1
  6. package/dist/array/impl/array-utils-search.mjs.map +1 -1
  7. package/dist/collections/imap-mapped.mjs +1 -1
  8. package/dist/collections/imap.mjs +1 -1
  9. package/dist/entry-point.mjs +8 -6
  10. package/dist/entry-point.mjs.map +1 -1
  11. package/dist/functional/async-result/async-result.d.mts +16 -0
  12. package/dist/functional/async-result/async-result.d.mts.map +1 -0
  13. package/dist/functional/async-result/async-result.mjs +3 -0
  14. package/dist/functional/async-result/async-result.mjs.map +1 -0
  15. package/dist/functional/async-result/impl/async-result-flat-map.d.mts +39 -0
  16. package/dist/functional/async-result/impl/async-result-flat-map.d.mts.map +1 -0
  17. package/dist/functional/async-result/impl/async-result-flat-map.mjs +25 -0
  18. package/dist/functional/async-result/impl/async-result-flat-map.mjs.map +1 -0
  19. package/dist/functional/async-result/impl/async-result-from-promise.d.mts +47 -0
  20. package/dist/functional/async-result/impl/async-result-from-promise.d.mts.map +1 -0
  21. package/dist/functional/async-result/impl/async-result-from-promise.mjs +11 -0
  22. package/dist/functional/async-result/impl/async-result-from-promise.mjs.map +1 -0
  23. package/dist/functional/async-result/impl/async-result-from-throwable.d.mts +53 -0
  24. package/dist/functional/async-result/impl/async-result-from-throwable.d.mts.map +1 -0
  25. package/dist/functional/async-result/impl/async-result-from-throwable.mjs +17 -0
  26. package/dist/functional/async-result/impl/async-result-from-throwable.mjs.map +1 -0
  27. package/dist/functional/async-result/impl/async-result-map-err.d.mts +35 -0
  28. package/dist/functional/async-result/impl/async-result-map-err.d.mts.map +1 -0
  29. package/dist/functional/async-result/impl/async-result-map-err.mjs +23 -0
  30. package/dist/functional/async-result/impl/async-result-map-err.mjs.map +1 -0
  31. package/dist/functional/async-result/impl/async-result-map.d.mts +45 -0
  32. package/dist/functional/async-result/impl/async-result-map.d.mts.map +1 -0
  33. package/dist/functional/async-result/impl/async-result-map.mjs +23 -0
  34. package/dist/functional/async-result/impl/async-result-map.mjs.map +1 -0
  35. package/dist/functional/async-result/impl/async-result-unwrap-or.d.mts +34 -0
  36. package/dist/functional/async-result/impl/async-result-unwrap-or.d.mts.map +1 -0
  37. package/dist/functional/async-result/impl/async-result-unwrap-or.mjs +10 -0
  38. package/dist/functional/async-result/impl/async-result-unwrap-or.mjs.map +1 -0
  39. package/dist/functional/async-result/impl/index.d.mts +8 -0
  40. package/dist/functional/async-result/impl/index.d.mts.map +1 -0
  41. package/dist/functional/async-result/impl/index.mjs +7 -0
  42. package/dist/functional/async-result/impl/index.mjs.map +1 -0
  43. package/dist/functional/async-result/impl/types.d.mts +45 -0
  44. package/dist/functional/async-result/impl/types.d.mts.map +1 -0
  45. package/dist/functional/async-result/impl/types.mjs +2 -0
  46. package/dist/functional/async-result/impl/types.mjs.map +1 -0
  47. package/dist/functional/async-result/index.d.mts +2 -0
  48. package/dist/functional/async-result/index.d.mts.map +1 -0
  49. package/dist/functional/async-result/index.mjs +3 -0
  50. package/dist/functional/async-result/index.mjs.map +1 -0
  51. package/dist/functional/index.d.mts +1 -0
  52. package/dist/functional/index.d.mts.map +1 -1
  53. package/dist/functional/index.mjs +8 -6
  54. package/dist/functional/index.mjs.map +1 -1
  55. package/dist/functional/optional/impl/index.d.mts +1 -0
  56. package/dist/functional/optional/impl/index.d.mts.map +1 -1
  57. package/dist/functional/optional/impl/index.mjs +1 -0
  58. package/dist/functional/optional/impl/index.mjs.map +1 -1
  59. package/dist/functional/optional/impl/optional-match.d.mts +48 -0
  60. package/dist/functional/optional/impl/optional-match.d.mts.map +1 -0
  61. package/dist/functional/optional/impl/optional-match.mjs +22 -0
  62. package/dist/functional/optional/impl/optional-match.mjs.map +1 -0
  63. package/dist/functional/result/impl/index.d.mts +4 -0
  64. package/dist/functional/result/impl/index.d.mts.map +1 -1
  65. package/dist/functional/result/impl/index.mjs +4 -0
  66. package/dist/functional/result/impl/index.mjs.map +1 -1
  67. package/dist/functional/result/impl/result-from-optional.d.mts +41 -0
  68. package/dist/functional/result/impl/result-from-optional.d.mts.map +1 -0
  69. package/dist/functional/result/impl/result-from-optional.mjs +24 -0
  70. package/dist/functional/result/impl/result-from-optional.mjs.map +1 -0
  71. package/dist/functional/result/impl/result-from-promise.d.mts +5 -0
  72. package/dist/functional/result/impl/result-from-promise.d.mts.map +1 -1
  73. package/dist/functional/result/impl/result-from-promise.mjs.map +1 -1
  74. package/dist/functional/result/impl/result-match.d.mts +47 -0
  75. package/dist/functional/result/impl/result-match.d.mts.map +1 -0
  76. package/dist/functional/result/impl/result-match.mjs +24 -0
  77. package/dist/functional/result/impl/result-match.mjs.map +1 -0
  78. package/dist/functional/result/impl/result-safe-try.d.mts +51 -0
  79. package/dist/functional/result/impl/result-safe-try.d.mts.map +1 -0
  80. package/dist/functional/result/impl/result-safe-try.mjs +24 -0
  81. package/dist/functional/result/impl/result-safe-try.mjs.map +1 -0
  82. package/dist/functional/result/impl/result-safe-unwrap.d.mts +36 -0
  83. package/dist/functional/result/impl/result-safe-unwrap.d.mts.map +1 -0
  84. package/dist/functional/result/impl/result-safe-unwrap.mjs +14 -0
  85. package/dist/functional/result/impl/result-safe-unwrap.mjs.map +1 -0
  86. package/dist/json/json.mjs +2 -2
  87. package/dist/number/refined-number-utils.mjs +1 -1
  88. package/dist/object/object.mjs +1 -1
  89. package/dist/others/debounce-function.d.mts.map +1 -1
  90. package/dist/others/debounce-function.mjs.map +1 -1
  91. package/dist/others/fast-deep-equal.d.mts.map +1 -1
  92. package/dist/others/fast-deep-equal.mjs +1 -1
  93. package/dist/others/fast-deep-equal.mjs.map +1 -1
  94. package/package.json +10 -10
  95. package/src/array/impl/array-utils-modification.test.mts +3 -3
  96. package/src/array/impl/array-utils-search.mts +22 -8
  97. package/src/array/impl/array-utils-shape-invariants.test.mts +3 -3
  98. package/src/array/impl/array-utils-transformation.test.mts +1 -1
  99. package/src/functional/async-result/async-result.mts +17 -0
  100. package/src/functional/async-result/impl/async-result-flat-map.mts +99 -0
  101. package/src/functional/async-result/impl/async-result-from-promise.mts +69 -0
  102. package/src/functional/async-result/impl/async-result-from-throwable.mts +78 -0
  103. package/src/functional/async-result/impl/async-result-map-err.mts +84 -0
  104. package/src/functional/async-result/impl/async-result-map.mts +94 -0
  105. package/src/functional/async-result/impl/async-result-unwrap-or.mts +42 -0
  106. package/src/functional/async-result/impl/index.mts +7 -0
  107. package/src/functional/async-result/impl/types.mts +51 -0
  108. package/src/functional/async-result/index.mts +1 -0
  109. package/src/functional/async-result.test.mts +290 -0
  110. package/src/functional/index.mts +1 -0
  111. package/src/functional/optional/impl/index.mts +1 -0
  112. package/src/functional/optional/impl/optional-match.mts +108 -0
  113. package/src/functional/optional.test.mts +79 -0
  114. package/src/functional/result/impl/index.mts +4 -0
  115. package/src/functional/result/impl/result-from-optional.mts +80 -0
  116. package/src/functional/result/impl/result-from-promise.mts +5 -0
  117. package/src/functional/result/impl/result-match.mts +108 -0
  118. package/src/functional/result/impl/result-safe-try.mts +110 -0
  119. package/src/functional/result/impl/result-safe-unwrap.mts +50 -0
  120. package/src/functional/result.test.mts +331 -0
  121. package/src/guard/is-error.test.mts +7 -7
  122. package/src/others/debounce-function.mts +5 -1
  123. package/src/others/fast-deep-equal.mts +9 -2
@@ -0,0 +1,108 @@
1
+ import { type UnknownOptional } from '../optional.mjs';
2
+ import { isSome } from './optional-is-some.mjs';
3
+ import { unwrap } from './optional-unwrap.mjs';
4
+ import { type Unwrap } from './types.mjs';
5
+
6
+ /**
7
+ * Folds an {@link Optional} into a plain value by applying one of two case
8
+ * handlers: `some` if the {@link Optional} contains a value, `none`
9
+ * otherwise. This is the function form of pattern matching on an
10
+ * {@link Optional}.
11
+ *
12
+ * Unlike {@link Optional.map}, the result is not wrapped in an
13
+ * {@link Optional} — both branches produce the final value directly.
14
+ *
15
+ * @example
16
+ *
17
+ * ```ts
18
+ * const doubledOrZero = Optional.match(Optional.some(21), {
19
+ * some: (value) => value * 2,
20
+ * none: () => 0,
21
+ * });
22
+ *
23
+ * assert.deepStrictEqual(doubledOrZero, 42);
24
+ *
25
+ * const matcher = Optional.match({
26
+ * some: (value: number) => `value: ${value}`,
27
+ * none: () => 'none',
28
+ * });
29
+ *
30
+ * assert.deepStrictEqual(matcher(Optional.some(3)), 'value: 3');
31
+ *
32
+ * assert.deepStrictEqual(matcher(Optional.none), 'none');
33
+ * ```
34
+ *
35
+ * @template O The input `UnknownOptional` type.
36
+ * @template S2 The type returned by the `some` case.
37
+ * @template N2 The type returned by the `none` case.
38
+ * @param optional The `Optional` to match on.
39
+ * @param cases An object with a `some` handler receiving the contained value
40
+ * and a `none` handler receiving nothing.
41
+ * @returns The value produced by whichever case handler was applied.
42
+ */
43
+ export function match<O extends UnknownOptional, S2, N2>(
44
+ optional: O,
45
+ cases: Readonly<{
46
+ some: (value: Unwrap<O>) => S2;
47
+ none: () => N2;
48
+ }>,
49
+ ): N2 | S2;
50
+
51
+ // Curried version
52
+ //
53
+ // The returned function is generic over the *whole* optional, mirroring the
54
+ // direct overload above; see the comment on the curried overload of
55
+ // `Optional.map` for why the parameter is a conditional type instead of a
56
+ // plain `Optional<S>`.
57
+ export function match<S, S2, N2>(
58
+ cases: Readonly<{
59
+ some: (value: S) => S2;
60
+ none: () => N2;
61
+ }>,
62
+ ): <O extends UnknownOptional>(
63
+ optional: Unwrap<O> extends S ? O : never,
64
+ ) => N2 | S2;
65
+
66
+ export function match<O extends UnknownOptional, S2, N2>(
67
+ ...args:
68
+ | readonly [
69
+ optional: O,
70
+ cases: Readonly<{
71
+ some: (value: Unwrap<O>) => S2;
72
+ none: () => N2;
73
+ }>,
74
+ ]
75
+ | readonly [
76
+ cases: Readonly<{
77
+ some: (value: Unwrap<O>) => S2;
78
+ none: () => N2;
79
+ }>,
80
+ ]
81
+ ): N2 | S2 | ((optional: O) => N2 | S2) {
82
+ switch (args.length) {
83
+ case 2: {
84
+ const [optional, cases] = args;
85
+
86
+ return matchImpl(optional, cases);
87
+ }
88
+
89
+ case 1: {
90
+ // Curried version: the only argument is the case handlers
91
+ const [cases] = args;
92
+
93
+ return (optional: O) => matchImpl(optional, cases);
94
+ }
95
+ }
96
+ }
97
+
98
+ const matchImpl = <O extends UnknownOptional, S2, N2>(
99
+ optional: O,
100
+ cases: Readonly<{
101
+ some: (value: Unwrap<O>) => S2;
102
+ none: () => N2;
103
+ }>,
104
+ ): N2 | S2 =>
105
+ isSome(optional)
106
+ ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
107
+ cases.some(unwrap(optional)!)
108
+ : cases.none();
@@ -800,4 +800,83 @@ describe('Optional test', () => {
800
800
  expect(Optional.unwrap(inner)).toBe(42);
801
801
  });
802
802
  });
803
+
804
+ describe('match', () => {
805
+ test('should apply the some case for Some', () => {
806
+ const result = Optional.match(Optional.some(21), {
807
+ some: (value) => value * 2,
808
+ none: () => 0,
809
+ });
810
+
811
+ expectType<typeof result, number>('=');
812
+
813
+ assert.deepStrictEqual(result, 42);
814
+ });
815
+
816
+ test('should apply the none case for None', () => {
817
+ const result = Optional.match(Optional.none, {
818
+ some: () => 'some',
819
+ none: () => 'none',
820
+ });
821
+
822
+ assert.deepStrictEqual(result, 'none');
823
+ });
824
+
825
+ test('should support Optional union inputs', () => {
826
+ const optional: Optional<number> = Optional.some(3);
827
+
828
+ const result = Optional.match(optional, {
829
+ some: (value) => `value: ${value}`,
830
+ none: () => 'none',
831
+ });
832
+
833
+ expectType<typeof result, string>('=');
834
+
835
+ assert.deepStrictEqual(result, 'value: 3');
836
+ });
837
+
838
+ test('curried version should work', () => {
839
+ const matcher = Optional.match({
840
+ some: (value: number) => `value: ${value}`,
841
+ none: () => 'none',
842
+ });
843
+
844
+ const result = matcher(Optional.some(3));
845
+
846
+ expectType<typeof result, string>('=');
847
+
848
+ assert.deepStrictEqual(result, 'value: 3');
849
+
850
+ assert.deepStrictEqual(matcher(Optional.none), 'none');
851
+ });
852
+
853
+ test('curried version should compose with Array.map and pipe', () => {
854
+ const matcher = Optional.match({
855
+ some: (value: number) => `some:${value}`,
856
+ none: () => 'none',
857
+ });
858
+
859
+ const optionals: readonly Optional<number>[] = [
860
+ Optional.some(1),
861
+ Optional.none,
862
+ ] as const;
863
+
864
+ assert.deepStrictEqual(optionals.map(matcher), ['some:1', 'none']);
865
+
866
+ const single: Optional<number> = Optional.some(3);
867
+
868
+ assert.deepStrictEqual(pipe(single).map(matcher).value, 'some:3');
869
+ });
870
+
871
+ test('should apply the some case for Some(undefined)', () => {
872
+ const makeOptional = (): Optional<undefined> => Optional.some(undefined);
873
+
874
+ const result = Optional.match(makeOptional(), {
875
+ some: (value) => `some: ${String(value)}`,
876
+ none: () => 'none',
877
+ });
878
+
879
+ assert.deepStrictEqual(result, 'some: undefined');
880
+ });
881
+ });
803
882
  });
@@ -2,6 +2,7 @@ export * from './result-err.mjs';
2
2
  export * from './result-expect-to-be.mjs';
3
3
  export * from './result-flat-map.mjs';
4
4
  export * from './result-fold.mjs';
5
+ export * from './result-from-optional.mjs';
5
6
  export * from './result-from-promise.mjs';
6
7
  export * from './result-from-throwable.mjs';
7
8
  export * from './result-is-err.mjs';
@@ -9,8 +10,11 @@ export * from './result-is-ok.mjs';
9
10
  export * from './result-is-result.mjs';
10
11
  export * from './result-map-err.mjs';
11
12
  export * from './result-map.mjs';
13
+ export * from './result-match.mjs';
12
14
  export * from './result-ok.mjs';
13
15
  export * from './result-or-else.mjs';
16
+ export * from './result-safe-try.mjs';
17
+ export * from './result-safe-unwrap.mjs';
14
18
  export * from './result-swap.mjs';
15
19
  export * from './result-to-optional.mjs';
16
20
  export * from './result-unwrap-err-or.mjs';
@@ -0,0 +1,80 @@
1
+ import { Optional, type UnknownOptional } from '../../optional/index.mjs';
2
+ import { type Result } from '../result.mjs';
3
+ import { err } from './result-err.mjs';
4
+ import { ok } from './result-ok.mjs';
5
+
6
+ /**
7
+ * Converts an {@link Optional} to a `Result` (the inverse of
8
+ * {@link Result.toOptional}, and the equivalent of Rust's `Option::ok_or`).
9
+ *
10
+ * If the {@link Optional} is `Some`, returns `Ok` with the contained value.
11
+ * If it is `None`, returns `Err` with the provided error value.
12
+ *
13
+ * @example
14
+ *
15
+ * ```ts
16
+ * assert.deepStrictEqual(
17
+ * Result.fromOptional(Optional.some(42), 'missing'),
18
+ * Result.ok(42),
19
+ * );
20
+ *
21
+ * assert.deepStrictEqual(
22
+ * Result.fromOptional(Optional.none, 'missing'),
23
+ * Result.err('missing'),
24
+ * );
25
+ *
26
+ * const withMissingError = Result.fromOptional('missing');
27
+ *
28
+ * assert.deepStrictEqual(withMissingError(Optional.some(1)), Result.ok(1));
29
+ *
30
+ * assert.deepStrictEqual(
31
+ * withMissingError(Optional.none),
32
+ * Result.err('missing'),
33
+ * );
34
+ * ```
35
+ *
36
+ * @template O The input `UnknownOptional` type.
37
+ * @template E The type of the error value used for the `None` case.
38
+ * @param optional The `Optional` to convert.
39
+ * @param error The error value to use if the `Optional` is `None`.
40
+ * @returns `Ok` with the contained value, or `Err` with `error`.
41
+ */
42
+ export function fromOptional<O extends UnknownOptional, const E>(
43
+ optional: O,
44
+ error: E,
45
+ ): Result<Optional.Unwrap<O>, E>;
46
+
47
+ // Curried version
48
+ export function fromOptional<const E>(
49
+ error: E,
50
+ ): <O extends UnknownOptional>(optional: O) => Result<Optional.Unwrap<O>, E>;
51
+
52
+ export function fromOptional<O extends UnknownOptional, const E>(
53
+ ...args: readonly [optional: O, error: E] | readonly [error: E]
54
+ ):
55
+ | Result<Optional.Unwrap<O>, E>
56
+ | ((optional: O) => Result<Optional.Unwrap<O>, E>) {
57
+ switch (args.length) {
58
+ case 2: {
59
+ const [optional, error] = args;
60
+
61
+ return fromOptionalImpl(optional, error);
62
+ }
63
+
64
+ case 1: {
65
+ // Curried version: the only argument is the error value
66
+ const [error] = args;
67
+
68
+ return (optional: O) => fromOptionalImpl(optional, error);
69
+ }
70
+ }
71
+ }
72
+
73
+ const fromOptionalImpl = <O extends UnknownOptional, E>(
74
+ optional: O,
75
+ error: E,
76
+ ): Result<Optional.Unwrap<O>, E> =>
77
+ Optional.isSome(optional)
78
+ ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
79
+ ok(Optional.unwrap(optional)!)
80
+ : err(error);
@@ -17,6 +17,11 @@ type UnwrapPromise<P extends Promise<unknown>> =
17
17
  * input Promise rejects, the `Result` will be `Err` with the rejection
18
18
  * reason.
19
19
  *
20
+ * The error channel is `unknown`, since the rejection reason is carried
21
+ * through untouched. For a concrete error type, use `AsyncResult.fromPromise`
22
+ * with a `mapError` function; called without one it behaves exactly as this
23
+ * function does.
24
+ *
20
25
  * @example
21
26
  *
22
27
  * ```ts
@@ -0,0 +1,108 @@
1
+ import { type UnknownResult } from '../result.mjs';
2
+ import { isOk } from './result-is-ok.mjs';
3
+ import { type UnwrapErr, type UnwrapOk } from './types.mjs';
4
+
5
+ /**
6
+ * Folds a `Result` into a plain value by applying one of two case handlers:
7
+ * `ok` if the `Result` is `Ok`, `err` otherwise. This is the function form of
8
+ * pattern matching on a `Result`.
9
+ *
10
+ * Unlike {@link Result.fold}, which maps both sides and returns another
11
+ * `Result`, `match` unwraps: both branches produce the final value directly.
12
+ *
13
+ * @example
14
+ *
15
+ * ```ts
16
+ * const doubledOrZero = Result.match(Result.ok(21), {
17
+ * ok: (value) => value * 2,
18
+ * err: () => 0,
19
+ * });
20
+ *
21
+ * assert.deepStrictEqual(doubledOrZero, 42);
22
+ *
23
+ * const matcher = Result.match({
24
+ * ok: (value: number) => value + 1,
25
+ * err: (error: string) => error.length,
26
+ * });
27
+ *
28
+ * assert.deepStrictEqual(matcher(Result.ok(1)), 2);
29
+ *
30
+ * assert.deepStrictEqual(matcher(Result.err('oops')), 4);
31
+ * ```
32
+ *
33
+ * @template R The input `UnknownResult` type.
34
+ * @template S2 The type returned by the `ok` case.
35
+ * @template E2 The type returned by the `err` case.
36
+ * @param result The `Result` to match on.
37
+ * @param cases An object with an `ok` handler receiving the success value and
38
+ * an `err` handler receiving the error value.
39
+ * @returns The value produced by whichever case handler was applied.
40
+ */
41
+ export function match<R extends UnknownResult, S2, E2>(
42
+ result: R,
43
+ cases: Readonly<{
44
+ ok: (value: UnwrapOk<R>) => S2;
45
+ err: (error: UnwrapErr<R>) => E2;
46
+ }>,
47
+ ): E2 | S2;
48
+
49
+ // Curried version
50
+ //
51
+ // The returned function is generic over the *whole* result, mirroring the
52
+ // direct overload above; see the comment on the curried overload of
53
+ // `Result.map` for why the parameter is a conditional type instead of a plain
54
+ // `Result<S, E>`. Both sides have to be checked here, hence the nested
55
+ // conditional.
56
+ export function match<S, E, S2, E2>(
57
+ cases: Readonly<{
58
+ ok: (value: S) => S2;
59
+ err: (error: E) => E2;
60
+ }>,
61
+ ): <R extends UnknownResult>(
62
+ result: UnwrapOk<R> extends S ? (UnwrapErr<R> extends E ? R : never) : never,
63
+ ) => E2 | S2;
64
+
65
+ export function match<R extends UnknownResult, S2, E2>(
66
+ ...args:
67
+ | readonly [
68
+ result: R,
69
+ cases: Readonly<{
70
+ ok: (value: UnwrapOk<R>) => S2;
71
+ err: (error: UnwrapErr<R>) => E2;
72
+ }>,
73
+ ]
74
+ | readonly [
75
+ cases: Readonly<{
76
+ ok: (value: UnwrapOk<R>) => S2;
77
+ err: (error: UnwrapErr<R>) => E2;
78
+ }>,
79
+ ]
80
+ ): E2 | S2 | ((result: R) => E2 | S2) {
81
+ switch (args.length) {
82
+ case 2: {
83
+ const [result, cases] = args;
84
+
85
+ return matchImpl(result, cases);
86
+ }
87
+
88
+ case 1: {
89
+ // Curried version: the only argument is the case handlers
90
+ const [cases] = args;
91
+
92
+ return (result: R) => matchImpl(result, cases);
93
+ }
94
+ }
95
+ }
96
+
97
+ const matchImpl = <R extends UnknownResult, S2, E2>(
98
+ result: R,
99
+ cases: Readonly<{
100
+ ok: (value: UnwrapOk<R>) => S2;
101
+ err: (error: UnwrapErr<R>) => E2;
102
+ }>,
103
+ ): E2 | S2 =>
104
+ isOk(result)
105
+ ? // eslint-disable-next-line total-functions/no-unsafe-type-assertion
106
+ cases.ok(result.value as UnwrapOk<R>)
107
+ : // eslint-disable-next-line total-functions/no-unsafe-type-assertion
108
+ cases.err(result.value as UnwrapErr<R>);
@@ -0,0 +1,110 @@
1
+ import { hasKey, isRecord } from '../../../guard/index.mjs';
2
+ import { type Err } from '../../../types.mjs';
3
+ import { type Result, type UnknownResult } from '../result.mjs';
4
+ import { type UnwrapErr, type UnwrapOk } from './types.mjs';
5
+
6
+ /**
7
+ * Runs a generator body in which `yield* Result.safeUnwrap(...)` unwraps `Ok`
8
+ * values and short-circuits on the first `Err`, emulating Rust's `?` operator
9
+ * (early-return error propagation).
10
+ *
11
+ * The body must return a `Result`. If every unwrapped `Result` is `Ok`, the
12
+ * returned value is the body's `Result`; otherwise it is the first `Err`
13
+ * encountered, and the rest of the body never runs — though the generator is
14
+ * closed rather than dropped, so `finally` blocks and `using` disposals
15
+ * pending in the body still run. An exception thrown by the body is not
16
+ * converted to an `Err`: it propagates out of `safeTry` (as a rejected
17
+ * `Promise` for an async body). Use {@link Result.fromThrowable} for that.
18
+ *
19
+ * Passing an async generator (`async function*`) returns a
20
+ * `Promise<Result<...>>` instead, so `await` can be used inside the body.
21
+ * Note that `yield*` inside an `async function*` awaits what it receives, so
22
+ * unwrapping an `Ok` whose value is itself a `Promise` yields the awaited
23
+ * value there while the static type still says `Promise<...>`; keep promises
24
+ * out of the `Ok` side of a `Result` unwrapped in an async body.
25
+ *
26
+ * @example
27
+ *
28
+ * ```ts
29
+ * const parseTwo = (a: string, b: string): Result<number, Error> =>
30
+ * Result.safeTry(function* () {
31
+ * const x = yield* Result.safeUnwrap(Num.safeParseInt(a));
32
+ *
33
+ * const y = yield* Result.safeUnwrap(Num.safeParseInt(b));
34
+ *
35
+ * return Result.ok(x + y);
36
+ * });
37
+ *
38
+ * assert.deepStrictEqual(parseTwo('1', '2'), Result.ok(3));
39
+ *
40
+ * assert.isTrue(Result.isErr(parseTwo('1', 'not a number')));
41
+ * ```
42
+ *
43
+ * @template Y The union of `Err` types unwrapped via
44
+ * {@link Result.safeUnwrap} (`never` if nothing is unwrapped).
45
+ * @template R2 The `Result` type returned by the body.
46
+ * @param body A generator function whose `yield*` expressions delegate to
47
+ * {@link Result.safeUnwrap} and whose return value is a `Result`.
48
+ * @returns The body's `Result`, or the first `Err` short-circuited out of it
49
+ * (wrapped in a `Promise` for an async generator body).
50
+ */
51
+ export function safeTry<Y extends Err<unknown>, R2 extends UnknownResult>(
52
+ body: () => Generator<Y, R2>,
53
+ ): Result<UnwrapOk<R2>, UnwrapErr<R2> | UnwrapErr<Y>>;
54
+
55
+ export function safeTry<Y extends Err<unknown>, R2 extends UnknownResult>(
56
+ body: () => AsyncGenerator<Y, R2>,
57
+ ): Promise<Result<UnwrapOk<R2>, UnwrapErr<R2> | UnwrapErr<Y>>>;
58
+
59
+ export function safeTry(
60
+ body:
61
+ | (() => AsyncGenerator<UnknownResult, UnknownResult>)
62
+ | (() => Generator<UnknownResult, UnknownResult>),
63
+ ): Promise<UnknownResult> | UnknownResult {
64
+ const generator = body();
65
+
66
+ // A yielded `Err` and a returned `Result` are both already the final
67
+ // answer, so the first step's `value` is the result in either case: the
68
+ // generator is closed rather than resumed after yielding.
69
+ const step = generator.next();
70
+
71
+ if (isSyncStep(step)) {
72
+ // Dropping a suspended generator would skip the `finally` blocks and
73
+ // `using` disposals still pending in the body; closing it runs them
74
+ // before the short-circuited `Err` is handed back. `isSyncGenerator` is
75
+ // what makes `return()` here synchronous rather than a floating promise.
76
+ if (step.done !== true && isSyncGenerator(generator, step)) {
77
+ generator.return(step.value);
78
+ }
79
+
80
+ return step.value;
81
+ }
82
+
83
+ return step.then(async (asyncStep) => {
84
+ if (asyncStep.done !== true) {
85
+ await generator.return(asyncStep.value);
86
+ }
87
+
88
+ return asyncStep.value;
89
+ });
90
+ }
91
+
92
+ // A sync generator's `next()` returns a plain `{ value, done }` object with
93
+ // own properties, while an async generator's returns a `Promise`, whose
94
+ // `then` lives on the prototype — so an own-key check on `done` separates the
95
+ // two.
96
+ const isSyncStep = (
97
+ value: unknown,
98
+ ): value is IteratorResult<UnknownResult, UnknownResult> =>
99
+ isRecord(value) && hasKey(value, 'done');
100
+
101
+ // The generator-side counterpart of `isSyncStep`: a step in the sync form can
102
+ // only have come from a sync generator, so the step is what narrows the
103
+ // generator that produced it — hence the predicate reads its second argument
104
+ // and not its first.
105
+ const isSyncGenerator = (
106
+ _generator:
107
+ | AsyncGenerator<UnknownResult, UnknownResult>
108
+ | Generator<UnknownResult, UnknownResult>,
109
+ step: unknown,
110
+ ): _generator is Generator<UnknownResult, UnknownResult> => isSyncStep(step);
@@ -0,0 +1,50 @@
1
+ import { type UnknownResult } from '../result.mjs';
2
+ import { isErr } from './result-is-err.mjs';
3
+ import { type NarrowToErr, type UnwrapOk } from './types.mjs';
4
+
5
+ /**
6
+ * Unwraps a `Result` inside a {@link Result.safeTry} generator body via
7
+ * `yield*`, emulating Rust's `?` operator.
8
+ *
9
+ * If the `Result` is `Ok`, the `yield*` expression evaluates to the contained
10
+ * value and execution continues. If it is `Err`, the error is yielded to
11
+ * {@link Result.safeTry}, which returns it immediately — the rest of the
12
+ * generator body never runs.
13
+ *
14
+ * This generator must only be delegated to (`yield*`) inside a
15
+ * {@link Result.safeTry} body; resuming it after it has yielded an `Err`
16
+ * throws.
17
+ *
18
+ * @example
19
+ *
20
+ * ```ts
21
+ * const result = Result.safeTry(function* () {
22
+ * const x = yield* Result.safeUnwrap(Result.ok(2));
23
+ *
24
+ * const y = yield* Result.safeUnwrap(Result.ok(3));
25
+ *
26
+ * return Result.ok(x + y);
27
+ * });
28
+ *
29
+ * assert.deepStrictEqual(result, Result.ok(5));
30
+ * ```
31
+ *
32
+ * @template R The input `UnknownResult` type.
33
+ * @param result The `Result` to unwrap.
34
+ * @returns A generator that yields the `Err` (if any) and returns the `Ok`
35
+ * value.
36
+ */
37
+ export function* safeUnwrap<R extends UnknownResult>(
38
+ result: R,
39
+ ): Generator<NarrowToErr<R>, UnwrapOk<R>, unknown> {
40
+ if (isErr(result)) {
41
+ yield result;
42
+
43
+ throw new Error(
44
+ '`safeUnwrap()` generator was resumed after yielding an `Err`; it must only be used via `yield*` inside `safeTry()`',
45
+ );
46
+ }
47
+
48
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
49
+ return result.value as UnwrapOk<R>;
50
+ }