ts-data-forge 1.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.
Files changed (143) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +534 -0
  3. package/package.json +101 -0
  4. package/src/array/array-utils-creation.test.mts +443 -0
  5. package/src/array/array-utils-modification.test.mts +197 -0
  6. package/src/array/array-utils-overload-type-error.test.mts +149 -0
  7. package/src/array/array-utils-reducing-value.test.mts +425 -0
  8. package/src/array/array-utils-search.test.mts +169 -0
  9. package/src/array/array-utils-set-op.test.mts +335 -0
  10. package/src/array/array-utils-slice-clamped.test.mts +113 -0
  11. package/src/array/array-utils-slicing.test.mts +316 -0
  12. package/src/array/array-utils-transformation.test.mts +790 -0
  13. package/src/array/array-utils-validation.test.mts +492 -0
  14. package/src/array/array-utils.mts +4000 -0
  15. package/src/array/array.test.mts +146 -0
  16. package/src/array/index.mts +2 -0
  17. package/src/array/tuple-utils.mts +519 -0
  18. package/src/array/tuple-utils.test.mts +518 -0
  19. package/src/collections/imap-mapped.mts +801 -0
  20. package/src/collections/imap-mapped.test.mts +860 -0
  21. package/src/collections/imap.mts +651 -0
  22. package/src/collections/imap.test.mts +932 -0
  23. package/src/collections/index.mts +6 -0
  24. package/src/collections/iset-mapped.mts +889 -0
  25. package/src/collections/iset-mapped.test.mts +1187 -0
  26. package/src/collections/iset.mts +682 -0
  27. package/src/collections/iset.test.mts +1084 -0
  28. package/src/collections/queue.mts +390 -0
  29. package/src/collections/queue.test.mts +282 -0
  30. package/src/collections/stack.mts +423 -0
  31. package/src/collections/stack.test.mts +225 -0
  32. package/src/expect-type.mts +206 -0
  33. package/src/functional/index.mts +4 -0
  34. package/src/functional/match.mts +300 -0
  35. package/src/functional/match.test.mts +177 -0
  36. package/src/functional/optional.mts +733 -0
  37. package/src/functional/optional.test.mts +619 -0
  38. package/src/functional/pipe.mts +212 -0
  39. package/src/functional/pipe.test.mts +85 -0
  40. package/src/functional/result.mts +1134 -0
  41. package/src/functional/result.test.mts +777 -0
  42. package/src/globals.d.mts +38 -0
  43. package/src/guard/has-key.mts +119 -0
  44. package/src/guard/has-key.test.mts +219 -0
  45. package/src/guard/index.mts +7 -0
  46. package/src/guard/is-non-empty-string.mts +108 -0
  47. package/src/guard/is-non-empty-string.test.mts +91 -0
  48. package/src/guard/is-non-null-object.mts +106 -0
  49. package/src/guard/is-non-null-object.test.mts +90 -0
  50. package/src/guard/is-primitive.mts +165 -0
  51. package/src/guard/is-primitive.test.mts +102 -0
  52. package/src/guard/is-record.mts +153 -0
  53. package/src/guard/is-record.test.mts +112 -0
  54. package/src/guard/is-type.mts +450 -0
  55. package/src/guard/is-type.test.mts +496 -0
  56. package/src/guard/key-is-in.mts +163 -0
  57. package/src/guard/key-is-in.test.mts +19 -0
  58. package/src/index.mts +10 -0
  59. package/src/iterator/index.mts +1 -0
  60. package/src/iterator/range.mts +120 -0
  61. package/src/iterator/range.test.mts +33 -0
  62. package/src/json/index.mts +1 -0
  63. package/src/json/json.mts +711 -0
  64. package/src/json/json.test.mts +628 -0
  65. package/src/number/branded-types/finite-number.mts +354 -0
  66. package/src/number/branded-types/finite-number.test.mts +135 -0
  67. package/src/number/branded-types/index.mts +26 -0
  68. package/src/number/branded-types/int.mts +278 -0
  69. package/src/number/branded-types/int.test.mts +140 -0
  70. package/src/number/branded-types/int16.mts +192 -0
  71. package/src/number/branded-types/int16.test.mts +170 -0
  72. package/src/number/branded-types/int32.mts +193 -0
  73. package/src/number/branded-types/int32.test.mts +170 -0
  74. package/src/number/branded-types/non-negative-finite-number.mts +223 -0
  75. package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
  76. package/src/number/branded-types/non-negative-int16.mts +187 -0
  77. package/src/number/branded-types/non-negative-int16.test.mts +201 -0
  78. package/src/number/branded-types/non-negative-int32.mts +187 -0
  79. package/src/number/branded-types/non-negative-int32.test.mts +204 -0
  80. package/src/number/branded-types/non-zero-finite-number.mts +229 -0
  81. package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
  82. package/src/number/branded-types/non-zero-int.mts +167 -0
  83. package/src/number/branded-types/non-zero-int.test.mts +177 -0
  84. package/src/number/branded-types/non-zero-int16.mts +196 -0
  85. package/src/number/branded-types/non-zero-int16.test.mts +195 -0
  86. package/src/number/branded-types/non-zero-int32.mts +196 -0
  87. package/src/number/branded-types/non-zero-int32.test.mts +197 -0
  88. package/src/number/branded-types/non-zero-safe-int.mts +196 -0
  89. package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
  90. package/src/number/branded-types/non-zero-uint16.mts +189 -0
  91. package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
  92. package/src/number/branded-types/non-zero-uint32.mts +189 -0
  93. package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
  94. package/src/number/branded-types/positive-finite-number.mts +241 -0
  95. package/src/number/branded-types/positive-finite-number.test.mts +204 -0
  96. package/src/number/branded-types/positive-int.mts +304 -0
  97. package/src/number/branded-types/positive-int.test.mts +176 -0
  98. package/src/number/branded-types/positive-int16.mts +188 -0
  99. package/src/number/branded-types/positive-int16.test.mts +197 -0
  100. package/src/number/branded-types/positive-int32.mts +188 -0
  101. package/src/number/branded-types/positive-int32.test.mts +197 -0
  102. package/src/number/branded-types/positive-safe-int.mts +187 -0
  103. package/src/number/branded-types/positive-safe-int.test.mts +210 -0
  104. package/src/number/branded-types/positive-uint16.mts +188 -0
  105. package/src/number/branded-types/positive-uint16.test.mts +203 -0
  106. package/src/number/branded-types/positive-uint32.mts +188 -0
  107. package/src/number/branded-types/positive-uint32.test.mts +203 -0
  108. package/src/number/branded-types/safe-int.mts +291 -0
  109. package/src/number/branded-types/safe-int.test.mts +170 -0
  110. package/src/number/branded-types/safe-uint.mts +187 -0
  111. package/src/number/branded-types/safe-uint.test.mts +176 -0
  112. package/src/number/branded-types/uint.mts +179 -0
  113. package/src/number/branded-types/uint.test.mts +158 -0
  114. package/src/number/branded-types/uint16.mts +186 -0
  115. package/src/number/branded-types/uint16.test.mts +170 -0
  116. package/src/number/branded-types/uint32.mts +218 -0
  117. package/src/number/branded-types/uint32.test.mts +170 -0
  118. package/src/number/enum/index.mts +2 -0
  119. package/src/number/enum/int8.mts +344 -0
  120. package/src/number/enum/int8.test.mts +180 -0
  121. package/src/number/enum/uint8.mts +293 -0
  122. package/src/number/enum/uint8.test.mts +164 -0
  123. package/src/number/index.mts +4 -0
  124. package/src/number/num.mts +604 -0
  125. package/src/number/num.test.mts +242 -0
  126. package/src/number/refined-number-utils.mts +566 -0
  127. package/src/object/index.mts +1 -0
  128. package/src/object/object.mts +447 -0
  129. package/src/object/object.test.mts +124 -0
  130. package/src/others/cast-mutable.mts +113 -0
  131. package/src/others/cast-readonly.mts +192 -0
  132. package/src/others/cast-readonly.test.mts +89 -0
  133. package/src/others/if-then.mts +98 -0
  134. package/src/others/if-then.test.mts +75 -0
  135. package/src/others/index.mts +7 -0
  136. package/src/others/map-nullable.mts +172 -0
  137. package/src/others/map-nullable.test.mts +297 -0
  138. package/src/others/memoize-function.mts +196 -0
  139. package/src/others/memoize-function.test.mts +168 -0
  140. package/src/others/tuple.mts +160 -0
  141. package/src/others/tuple.test.mts +11 -0
  142. package/src/others/unknown-to-string.mts +215 -0
  143. package/src/others/unknown-to-string.test.mts +114 -0
@@ -0,0 +1,619 @@
1
+ /* eslint-disable @typescript-eslint/no-confusing-void-expression */
2
+ import { expectType } from '../expect-type.mjs';
3
+ import { Optional } from './optional.mjs';
4
+ import { pipe } from './pipe.mjs';
5
+
6
+ describe('Optional', () => {
7
+ describe('isOptional', () => {
8
+ test('should return true for Some values', () => {
9
+ expect(Optional.isOptional(Optional.some(42))).toBe(true);
10
+ expect(Optional.isOptional(Optional.some('hello'))).toBe(true);
11
+ expect(Optional.isOptional(Optional.some(null))).toBe(true);
12
+ expect(Optional.isOptional(Optional.some(undefined))).toBe(true);
13
+ });
14
+
15
+ test('should return true for None value', () => {
16
+ expect(Optional.isOptional(Optional.none)).toBe(true);
17
+ });
18
+
19
+ test('should return false for non-Optional values', () => {
20
+ expect(Optional.isOptional(42)).toBe(false);
21
+ expect(Optional.isOptional('hello')).toBe(false);
22
+ expect(Optional.isOptional(null)).toBe(false);
23
+ expect(Optional.isOptional(undefined)).toBe(false);
24
+ expect(Optional.isOptional({})).toBe(false);
25
+ expect(Optional.isOptional({ type: 'fake', value: 42 })).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe('some', () => {
30
+ test('should create a Some variant with the provided value', () => {
31
+ const someNumber = Optional.some(42);
32
+ expect(Optional.isSome(someNumber)).toBe(true);
33
+ expect(Optional.unwrap(someNumber)).toBe(42);
34
+
35
+ const someString = Optional.some('hello');
36
+ expect(Optional.isSome(someString)).toBe(true);
37
+ expect(Optional.unwrap(someString)).toBe('hello');
38
+
39
+ const someObject = Optional.some({ name: 'Alice', age: 30 });
40
+ expect(Optional.isSome(someObject)).toBe(true);
41
+ expect(Optional.unwrap(someObject)).toStrictEqual({
42
+ name: 'Alice',
43
+ age: 30,
44
+ });
45
+ });
46
+
47
+ test('should preserve const types', () => {
48
+ expectTypeOf(Optional.some('test' as const)).toEqualTypeOf<
49
+ Optional.Some<'test'>
50
+ >();
51
+ });
52
+ });
53
+
54
+ describe('none', () => {
55
+ test('should be a singleton None value', () => {
56
+ expect(Optional.isNone(Optional.none)).toBe(true);
57
+ expect(Optional.isSome(Optional.none)).toBe(false);
58
+ expect(Optional.unwrapOr(Optional.none, undefined)).toBe(undefined);
59
+ });
60
+
61
+ test('should always reference the same instance', () => {
62
+ expect(Optional.none).toBe(Optional.none);
63
+ });
64
+ });
65
+
66
+ describe('isSome and isNone', () => {
67
+ test('should correctly identify Some values', () => {
68
+ const some = Optional.some(42);
69
+ expect(Optional.isSome(some)).toBe(true);
70
+ expect(Optional.isNone(some)).toBe(false);
71
+ });
72
+
73
+ test('should correctly identify None values', () => {
74
+ const none = Optional.none;
75
+ expect(Optional.isSome(none)).toBe(false);
76
+ expect(Optional.isNone(none)).toBe(true);
77
+ });
78
+
79
+ test('should act as type guards', () => {
80
+ const optional = Optional.some(42) as Optional<number>;
81
+
82
+ if (Optional.isSome(optional)) {
83
+ expectType<typeof optional, Optional.Some<number>>('=');
84
+ }
85
+
86
+ if (Optional.isNone(optional)) {
87
+ expectType<typeof optional, Optional.None>('<=');
88
+ }
89
+ });
90
+ });
91
+
92
+ describe('map', () => {
93
+ test('should map over Some values', () => {
94
+ const some = Optional.some(5);
95
+ const mapped = Optional.map(some, (x) => x * 2);
96
+
97
+ expect(Optional.isSome(mapped)).toBe(true);
98
+ if (Optional.isSome(mapped)) {
99
+ expect(Optional.unwrap(mapped)).toBe(10);
100
+ }
101
+ });
102
+
103
+ test('should return None for None values', () => {
104
+ const none = Optional.none;
105
+ const mapped = Optional.map(none, (x: never) => x * 2);
106
+
107
+ expect(Optional.isNone(mapped)).toBe(true);
108
+ });
109
+
110
+ test('should support chaining', () => {
111
+ const result = Optional.map(
112
+ Optional.map(Optional.some('hello'), (s) => s.toUpperCase()),
113
+ (s) => s.length,
114
+ );
115
+
116
+ if (Optional.isSome(result)) {
117
+ expect(Optional.unwrap(result)).toBe(5);
118
+ }
119
+ });
120
+
121
+ test('should preserve types correctly', () => {
122
+ const some = Optional.some(42);
123
+ expectTypeOf(Optional.map(some, (x) => x.toString())).toEqualTypeOf<
124
+ Optional<string>
125
+ >();
126
+ });
127
+
128
+ test('should support curried form', () => {
129
+ const doubler = Optional.map((x: number) => x * 2);
130
+
131
+ const some = Optional.some(5);
132
+ const mapped = doubler(some);
133
+
134
+ expect(Optional.isSome(mapped)).toBe(true);
135
+ if (Optional.isSome(mapped)) {
136
+ expect(Optional.unwrap(mapped)).toBe(10);
137
+ }
138
+
139
+ const none = Optional.none;
140
+ const mappedNone = doubler(none);
141
+ expect(Optional.isNone(mappedNone)).toBe(true);
142
+ });
143
+
144
+ test('should work with pipe when curried', () => {
145
+ const doubler = Optional.map((x: number) => x * 2);
146
+ const toStringFn = Optional.map((x: number) => x.toString());
147
+
148
+ const result = pipe(Optional.some(5)).map(doubler).map(toStringFn).value;
149
+
150
+ expect(Optional.isSome(result)).toBe(true);
151
+ if (Optional.isSome(result)) {
152
+ expect(Optional.unwrap(result)).toBe('10');
153
+ }
154
+ });
155
+ });
156
+
157
+ describe('unwrap', () => {
158
+ test('should return the value for Some', () => {
159
+ expect(Optional.unwrap(Optional.some(42))).toBe(42);
160
+ expect(Optional.unwrap(Optional.some('hello'))).toBe('hello');
161
+ expect(Optional.unwrap(Optional.some(null))).toBe(null);
162
+ });
163
+
164
+ test('should return undefined for None', () => {
165
+ expect(Optional.unwrapOr(Optional.none, undefined)).toBe(undefined);
166
+ });
167
+
168
+ test('should have correct return types', () => {
169
+ const someNumber = Optional.some(42);
170
+ expectTypeOf(Optional.unwrap(someNumber)).toExtend<number | undefined>();
171
+
172
+ const none = Optional.none;
173
+ expectTypeOf(Optional.unwrapOr(none, undefined)).toExtend<undefined>();
174
+ });
175
+ });
176
+
177
+ describe('unwrapThrow', () => {
178
+ test('should return the value for Some', () => {
179
+ expect(Optional.unwrapThrow(Optional.some(42))).toBe(42);
180
+ expect(Optional.unwrapThrow(Optional.some('hello'))).toBe('hello');
181
+ });
182
+
183
+ test('should throw for None', () => {
184
+ expect(() => Optional.unwrapThrow(Optional.none)).toThrow(
185
+ '`unwrapThrow()` has failed because it is `None`',
186
+ );
187
+ });
188
+
189
+ test('should have correct return types', () => {
190
+ const someNumber = Optional.some(42);
191
+ expectTypeOf(Optional.unwrapThrow(someNumber)).toExtend<number>();
192
+ });
193
+ });
194
+
195
+ describe('unwrapOr', () => {
196
+ test('should return the value for Some', () => {
197
+ expect(Optional.unwrapOr(Optional.some(42), 0)).toBe(42);
198
+ expect(Optional.unwrapOr(Optional.some('hello'), 'default')).toBe(
199
+ 'hello',
200
+ );
201
+ });
202
+
203
+ test('should return the default value for None', () => {
204
+ expect(Optional.unwrapOr(Optional.none, 0)).toBe(0);
205
+ expect(Optional.unwrapOr(Optional.none, 'default')).toBe('default');
206
+ });
207
+
208
+ test('should have correct return types', () => {
209
+ const someNumber = Optional.some(42);
210
+ expectTypeOf(Optional.unwrapOr(someNumber, 0)).toExtend<number>();
211
+
212
+ expectTypeOf(Optional.unwrapOr(someNumber, 'default')).toExtend<
213
+ number | string
214
+ >();
215
+
216
+ const none = Optional.none;
217
+ expectTypeOf(Optional.unwrapOr(none, 'default')).toExtend<string>();
218
+ });
219
+
220
+ test('should support curried form', () => {
221
+ const unwrapWithDefault = Optional.unwrapOr(42);
222
+
223
+ const someValue = Optional.some(100);
224
+ const result = unwrapWithDefault(someValue);
225
+ expect(result).toBe(100);
226
+
227
+ const noneValue = Optional.none;
228
+ const defaultResult = unwrapWithDefault(noneValue);
229
+ expect(defaultResult).toBe(42);
230
+ });
231
+
232
+ test('should work with pipe when curried', () => {
233
+ const unwrapWithDefault = Optional.unwrapOr('default');
234
+
235
+ const someResult = pipe(Optional.some('hello')).map(
236
+ unwrapWithDefault,
237
+ ).value;
238
+ expect(someResult).toBe('hello');
239
+
240
+ const noneResult = pipe(Optional.none).map(unwrapWithDefault).value;
241
+ expect(noneResult).toBe('default');
242
+ });
243
+ });
244
+
245
+ describe('expectToBe', () => {
246
+ test('should return the value for Some', () => {
247
+ const expectNumber = Optional.expectToBe<number>('Expected a number');
248
+ expect(expectNumber(Optional.some(42))).toBe(42);
249
+ });
250
+
251
+ test('should throw with custom message for None', () => {
252
+ const expectNumber = Optional.expectToBe<number>('Expected a number');
253
+ expect(() => expectNumber(Optional.none)).toThrow('Expected a number');
254
+ });
255
+
256
+ test('should be curried', () => {
257
+ const expectValidId = Optional.expectToBe<string>('ID is required');
258
+
259
+ const id1 = Optional.some('user-123');
260
+ const id2 = Optional.none;
261
+
262
+ expect(expectValidId(id1)).toBe('user-123');
263
+ expect(() => expectValidId(id2)).toThrow('ID is required');
264
+ });
265
+
266
+ test('should support curried form', () => {
267
+ const getValue = Optional.expectToBe('Value must exist');
268
+
269
+ const someValue = Optional.some('important data');
270
+ const result = getValue(someValue);
271
+ expect(result).toBe('important data');
272
+
273
+ const noneValue = Optional.none;
274
+ expect(() => getValue(noneValue)).toThrow('Value must exist');
275
+ });
276
+
277
+ test('should work with pipe when curried', () => {
278
+ const expectUser = Optional.expectToBe('User not found');
279
+
280
+ const someResult = pipe(Optional.some({ name: 'Alice', age: 30 })).map(
281
+ expectUser,
282
+ ).value;
283
+ expect(someResult).toStrictEqual({ name: 'Alice', age: 30 });
284
+
285
+ expect(() => pipe(Optional.none).map(expectUser).value).toThrow(
286
+ 'User not found',
287
+ );
288
+ });
289
+ });
290
+
291
+ describe('type utilities', () => {
292
+ test('should correctly unwrap types', () => {
293
+ type SomeNumber = Optional.Some<number>;
294
+ type UnwrappedNumber = Optional.Unwrap<SomeNumber>;
295
+ expectType<UnwrappedNumber, number>('=');
296
+
297
+ type None = Optional.None;
298
+ type UnwrappedNone = Optional.Unwrap<None>;
299
+ expectType<UnwrappedNone, never>('=');
300
+ });
301
+
302
+ test('should correctly narrow types', () => {
303
+ type MaybeNumber = Optional<number>;
304
+
305
+ type OnlySome = Optional.NarrowToSome<MaybeNumber>;
306
+ expectType<OnlySome, Optional.Some<number>>('=');
307
+
308
+ type OnlyNone = Optional.NarrowToNone<MaybeNumber>;
309
+ expectType<OnlyNone, Optional.None>('=');
310
+ });
311
+ });
312
+
313
+ describe('flatMap', () => {
314
+ test('should chain operations that return Optional', () => {
315
+ const parseNumber = (s: string): Optional<number> => {
316
+ const n = Number(s);
317
+ return Number.isNaN(n) ? Optional.none : Optional.some(n);
318
+ };
319
+
320
+ const result = Optional.flatMap(Optional.some('42'), parseNumber);
321
+ if (Optional.isSome(result)) {
322
+ expect(Optional.unwrap(result)).toBe(42);
323
+ }
324
+
325
+ const invalid = Optional.flatMap(Optional.some('abc'), parseNumber);
326
+ expect(Optional.isNone(invalid)).toBe(true);
327
+ });
328
+
329
+ test('should return None if input is None', () => {
330
+ const result = Optional.flatMap(Optional.none, (_: never) =>
331
+ Optional.some(42),
332
+ );
333
+ expect(Optional.isNone(result)).toBe(true);
334
+ });
335
+
336
+ test('should support chaining multiple flatMaps', () => {
337
+ const parseNumber = (s: string): Optional<number> => {
338
+ const n = Number(s);
339
+ return Number.isNaN(n) ? Optional.none : Optional.some(n);
340
+ };
341
+
342
+ const divideBy =
343
+ (divisor: number) =>
344
+ (n: number): Optional<number> =>
345
+ divisor === 0 ? Optional.none : Optional.some(n / divisor);
346
+
347
+ const intermediate = Optional.flatMap(Optional.some('100'), parseNumber);
348
+ const result = Optional.flatMap(intermediate, divideBy(2));
349
+ if (Optional.isSome(result)) {
350
+ expect(Optional.unwrap(result)).toBe(50);
351
+ }
352
+ });
353
+
354
+ test('should support curried form', () => {
355
+ const parseNumber = (s: string): Optional<number> => {
356
+ const n = Number(s);
357
+ return Number.isNaN(n) ? Optional.none : Optional.some(n);
358
+ };
359
+
360
+ const parser = Optional.flatMap(parseNumber);
361
+
362
+ const result = parser(Optional.some('42'));
363
+ expect(Optional.isSome(result)).toBe(true);
364
+ if (Optional.isSome(result)) {
365
+ expect(Optional.unwrap(result)).toBe(42);
366
+ }
367
+
368
+ const invalid = parser(Optional.some('abc'));
369
+ expect(Optional.isNone(invalid)).toBe(true);
370
+
371
+ const noneResult = parser(Optional.none);
372
+ expect(Optional.isNone(noneResult)).toBe(true);
373
+ });
374
+
375
+ test('should work with pipe when curried', () => {
376
+ const parseNumber = (s: string): Optional<number> => {
377
+ const n = Number(s);
378
+ return Number.isNaN(n) ? Optional.none : Optional.some(n);
379
+ };
380
+
381
+ const doubleIfPositive = (n: number): Optional<number> =>
382
+ n > 0 ? Optional.some(n * 2) : Optional.none;
383
+
384
+ const parser = Optional.flatMap(parseNumber);
385
+ const doubler = Optional.flatMap(doubleIfPositive);
386
+
387
+ const result = pipe(Optional.some('42')).map(parser).map(doubler).value;
388
+
389
+ expect(Optional.isSome(result)).toBe(true);
390
+ if (Optional.isSome(result)) {
391
+ expect(Optional.unwrap(result)).toBe(84);
392
+ }
393
+ });
394
+ });
395
+
396
+ describe('filter', () => {
397
+ test('should keep Some values that match predicate', () => {
398
+ const someEven = Optional.some(4);
399
+ const filtered = Optional.filter(someEven, (x) => x % 2 === 0);
400
+ if (Optional.isSome(filtered)) {
401
+ expect(Optional.unwrap(filtered)).toBe(4);
402
+ }
403
+ });
404
+
405
+ test('should return None for Some values that do not match predicate', () => {
406
+ const someOdd = Optional.some(5);
407
+ const filtered = Optional.filter(someOdd, (x) => x % 2 === 0);
408
+ expect(Optional.isNone(filtered)).toBe(true);
409
+ });
410
+
411
+ test('should return None if input is None', () => {
412
+ const filtered = Optional.filter(Optional.none, (_: never) => true);
413
+ expect(Optional.isNone(filtered)).toBe(true);
414
+ });
415
+
416
+ test('should support curried form', () => {
417
+ const evenFilter = Optional.filter((x: number) => x % 2 === 0);
418
+
419
+ const someEven = Optional.some(4);
420
+ const filtered = evenFilter(someEven);
421
+ expect(Optional.isSome(filtered)).toBe(true);
422
+ if (Optional.isSome(filtered)) {
423
+ expect(Optional.unwrap(filtered)).toBe(4);
424
+ }
425
+
426
+ const someOdd = Optional.some(5);
427
+ const filteredOdd = evenFilter(someOdd);
428
+ expect(Optional.isNone(filteredOdd)).toBe(true);
429
+
430
+ const noneResult = evenFilter(Optional.none);
431
+ expect(Optional.isNone(noneResult)).toBe(true);
432
+ });
433
+
434
+ test('should work with pipe when curried', () => {
435
+ const evenFilter = Optional.filter((x: number) => x % 2 === 0);
436
+ const positiveFilter = Optional.filter((x: number) => x > 0);
437
+
438
+ const result = pipe(Optional.some(4))
439
+ .map(evenFilter)
440
+ .map(positiveFilter).value;
441
+
442
+ expect(Optional.isSome(result)).toBe(true);
443
+ if (Optional.isSome(result)) {
444
+ expect(Optional.unwrap(result)).toBe(4);
445
+ }
446
+
447
+ const filtered = pipe(Optional.some(3)).map(evenFilter).value;
448
+
449
+ expect(Optional.isNone(filtered)).toBe(true);
450
+ });
451
+ });
452
+
453
+ describe('orElse', () => {
454
+ test('should return the first Optional if it is Some', () => {
455
+ const primary = Optional.some(42);
456
+ const fallback = Optional.some(100);
457
+ const result = Optional.orElse(primary, fallback);
458
+ if (Optional.isSome(result)) {
459
+ expect(Optional.unwrap(result)).toBe(42);
460
+ }
461
+ });
462
+
463
+ test('should return the alternative if the first is None', () => {
464
+ const primary = Optional.none;
465
+ const fallback = Optional.some('default');
466
+ const result = Optional.orElse(primary, fallback);
467
+ if (Optional.isSome(result)) {
468
+ expect(Optional.unwrap(result)).toBe('default');
469
+ }
470
+ });
471
+
472
+ test('should return None if both are None', () => {
473
+ const result = Optional.orElse(Optional.none, Optional.none);
474
+ expect(Optional.isNone(result)).toBe(true);
475
+ });
476
+
477
+ test('should support curried form', () => {
478
+ const fallbackTo = Optional.orElse(Optional.some('fallback'));
479
+
480
+ const someValue = Optional.some('primary');
481
+ const result = fallbackTo(someValue);
482
+ expect(Optional.isSome(result)).toBe(true);
483
+ if (Optional.isSome(result)) {
484
+ expect(Optional.unwrap(result)).toBe('primary');
485
+ }
486
+
487
+ const noneValue = Optional.none;
488
+ const fallbackResult = fallbackTo(noneValue);
489
+ expect(Optional.isSome(fallbackResult)).toBe(true);
490
+ if (Optional.isSome(fallbackResult)) {
491
+ expect(Optional.unwrap(fallbackResult)).toBe('fallback');
492
+ }
493
+ });
494
+
495
+ test('should work with pipe when curried', () => {
496
+ const fallbackTo = Optional.orElse(Optional.some('backup'));
497
+
498
+ const someResult = pipe(Optional.some('original')).map(fallbackTo).value;
499
+ expect(Optional.isSome(someResult)).toBe(true);
500
+ if (Optional.isSome(someResult)) {
501
+ expect(Optional.unwrap(someResult)).toBe('original');
502
+ }
503
+
504
+ const noneResult = pipe(Optional.none).map(fallbackTo).value;
505
+ expect(Optional.isSome(noneResult)).toBe(true);
506
+ if (Optional.isSome(noneResult)) {
507
+ expect(Optional.unwrap(noneResult)).toBe('backup');
508
+ }
509
+ });
510
+ });
511
+
512
+ describe('zip', () => {
513
+ test('should combine two Some values into a tuple', () => {
514
+ const a = Optional.some(1);
515
+ const b = Optional.some('hello');
516
+ const zipped = Optional.zip(a, b);
517
+ if (Optional.isSome(zipped)) {
518
+ expect(Optional.unwrap(zipped)).toStrictEqual([1, 'hello']);
519
+ }
520
+ });
521
+
522
+ test('should return None if first is None', () => {
523
+ const a = Optional.none;
524
+ const b = Optional.some('hello');
525
+ const zipped = Optional.zip(a, b);
526
+ expect(Optional.isNone(zipped)).toBe(true);
527
+ });
528
+
529
+ test('should return None if second is None', () => {
530
+ const a = Optional.some(1);
531
+ const b = Optional.none;
532
+ const zipped = Optional.zip(a, b);
533
+ expect(Optional.isNone(zipped)).toBe(true);
534
+ });
535
+
536
+ test('should return None if both are None', () => {
537
+ const zipped = Optional.zip(Optional.none, Optional.none);
538
+ expect(Optional.isNone(zipped)).toBe(true);
539
+ });
540
+ });
541
+
542
+ describe('fromNullable', () => {
543
+ test('should convert non-null values to Some', () => {
544
+ const helloOpt = Optional.fromNullable('hello');
545
+ if (Optional.isSome(helloOpt))
546
+ expect(Optional.unwrap(helloOpt)).toBe('hello');
547
+
548
+ const numOpt = Optional.fromNullable(42);
549
+ if (Optional.isSome(numOpt)) expect(Optional.unwrap(numOpt)).toBe(42);
550
+
551
+ const zeroOpt = Optional.fromNullable(0);
552
+ if (Optional.isSome(zeroOpt)) expect(Optional.unwrap(zeroOpt)).toBe(0);
553
+
554
+ const emptyOpt = Optional.fromNullable('');
555
+ if (Optional.isSome(emptyOpt)) expect(Optional.unwrap(emptyOpt)).toBe('');
556
+
557
+ const falseOpt = Optional.fromNullable(false);
558
+ if (Optional.isSome(falseOpt))
559
+ expect(Optional.unwrap(falseOpt)).toBe(false);
560
+ });
561
+
562
+ test('should convert null to None', () => {
563
+ expect(Optional.isNone(Optional.fromNullable(null))).toBe(true);
564
+ });
565
+
566
+ test('should convert undefined to None', () => {
567
+ expect(Optional.isNone(Optional.fromNullable(undefined))).toBe(true);
568
+ });
569
+
570
+ test('should work with union types', () => {
571
+ const value: string | null = 'test';
572
+ expectTypeOf(Optional.fromNullable(value)).toExtend<Optional<string>>();
573
+ });
574
+ });
575
+
576
+ describe('toNullable', () => {
577
+ test('should convert Some to its value', () => {
578
+ expect(Optional.toNullable(Optional.some(42))).toBe(42);
579
+ expect(Optional.toNullable(Optional.some('hello'))).toBe('hello');
580
+ expect(Optional.toNullable(Optional.some(null))).toBe(null);
581
+ });
582
+
583
+ test('should convert None to null', () => {
584
+ expect(Optional.toNullable(Optional.none)).toBe(undefined);
585
+ });
586
+
587
+ test('should have correct return type', () => {
588
+ const some = Optional.some(42);
589
+ expectTypeOf(Optional.toNullable(some)).toExtend<number | undefined>();
590
+ });
591
+ });
592
+
593
+ describe('edge cases', () => {
594
+ test('should handle undefined as a Some value', () => {
595
+ const someUndefined = Optional.some(undefined);
596
+ expect(Optional.isSome(someUndefined)).toBe(true);
597
+ const unwrappedUndefined = Optional.unwrap(someUndefined);
598
+ expect(unwrappedUndefined).toBe(undefined);
599
+
600
+ // This is different from None
601
+ expect(someUndefined).not.toBe(Optional.none);
602
+ });
603
+
604
+ test('should handle null as a Some value', () => {
605
+ const someNull = Optional.some(null);
606
+ expect(Optional.isSome(someNull)).toBe(true);
607
+ expect(Optional.unwrap(someNull)).toBe(null);
608
+ });
609
+
610
+ test('should handle nested Optionals', () => {
611
+ const nested = Optional.some(Optional.some(42));
612
+ expect(Optional.isSome(nested)).toBe(true);
613
+
614
+ const inner = Optional.unwrap(nested);
615
+ expect(Optional.isOptional(inner)).toBe(true);
616
+ expect(Optional.unwrap(inner)).toBe(42);
617
+ });
618
+ });
619
+ });