@nunofyobiz/effect-extras 0.0.1

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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/index.d.ts +3703 -0
  4. package/dist/index.js +1006 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +103 -0
  7. package/src/ArrayX/ArrayX.ts +818 -0
  8. package/src/ArrayX/index.ts +1 -0
  9. package/src/BigIntX/BigIntX.ts +35 -0
  10. package/src/BigIntX/index.ts +1 -0
  11. package/src/BooleanX/BooleanX.ts +24 -0
  12. package/src/BooleanX/index.ts +1 -0
  13. package/src/DurationX/DurationX.ts +178 -0
  14. package/src/DurationX/index.ts +1 -0
  15. package/src/EffectX/EffectX.ts +183 -0
  16. package/src/EffectX/index.ts +1 -0
  17. package/src/FormDataX/FormDataX.ts +57 -0
  18. package/src/FormDataX/index.ts +1 -0
  19. package/src/MapX/MapX.ts +54 -0
  20. package/src/MapX/index.ts +1 -0
  21. package/src/NonNullableX/NonNullableX.ts +290 -0
  22. package/src/NonNullableX/index.ts +2 -0
  23. package/src/NumberX/NumberX.ts +282 -0
  24. package/src/NumberX/index.ts +1 -0
  25. package/src/OptionX/OptionX.ts +234 -0
  26. package/src/OptionX/index.ts +1 -0
  27. package/src/OrderX/OrderX.ts +35 -0
  28. package/src/OrderX/index.ts +1 -0
  29. package/src/PredicateX/PredicateX.ts +98 -0
  30. package/src/PredicateX/index.ts +1 -0
  31. package/src/PromiseX/PromiseX.ts +32 -0
  32. package/src/PromiseX/index.ts +1 -0
  33. package/src/RecordX/RecordX.ts +478 -0
  34. package/src/RecordX/index.ts +1 -0
  35. package/src/ResultX/ResultX.ts +53 -0
  36. package/src/ResultX/index.ts +1 -0
  37. package/src/SchemaX/SchemaX.ts +324 -0
  38. package/src/SchemaX/index.ts +1 -0
  39. package/src/SetX/SetX.ts +160 -0
  40. package/src/SetX/index.ts +1 -0
  41. package/src/StringX/StringX.ts +97 -0
  42. package/src/StringX/index.ts +1 -0
  43. package/src/StructX/StructX.ts +310 -0
  44. package/src/StructX/index.ts +1 -0
  45. package/src/These/These.ts +1173 -0
  46. package/src/These/index.ts +1 -0
  47. package/src/index.ts +20 -0
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Record` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Array, Option, Order, Predicate, Record, pipe } from "effect";
7
+ import { dual } from "effect/Function";
8
+ import { ArrayX } from "../ArrayX";
9
+
10
+ /**
11
+ * Returns `true` when `record` has at least one entry, narrowing it to a
12
+ * known-non-empty `Record`.
13
+ *
14
+ * The negation of `Record.isEmptyRecord`, packaged as a type guard so it reads
15
+ * naturally at call sites that want to branch on "this record has something in
16
+ * it" without a manual `!Record.isEmptyRecord(...)`.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { RecordX } from "@nunofyobiz/effect-extras"
21
+ *
22
+ * assert.deepStrictEqual(RecordX.isNonEmptyRecord({ a: 1 }), true)
23
+ * assert.deepStrictEqual(RecordX.isNonEmptyRecord({}), false)
24
+ * ```
25
+ *
26
+ * @category guards
27
+ * @since 0.0.0
28
+ */
29
+ export const isNonEmptyRecord = <K extends PropertyKey, V>(
30
+ record: Record<K, V>,
31
+ ): record is Record<K, V> => pipe(record, Predicate.not(Record.isEmptyRecord));
32
+
33
+ /**
34
+ * Modifies the value at `key` in `self` with `f`, leaving the record unchanged
35
+ * if the key doesn't exist.
36
+ *
37
+ * v4's `Record.modify` returns `Option<Record>` — `None` when the key is
38
+ * absent. This helper picks the "do nothing if absent" semantics that v3's
39
+ * `Record.modify` had implicitly, and that most call sites assume. The modifier
40
+ * is never invoked when the key is missing.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { pipe } from "effect"
45
+ * import { RecordX } from "@nunofyobiz/effect-extras"
46
+ *
47
+ * // data-first
48
+ * assert.deepStrictEqual(
49
+ * RecordX.modifyIfExists({ a: 1, b: 2 }, "b", (n) => n * 10),
50
+ * { a: 1, b: 20 },
51
+ * )
52
+ *
53
+ * // a missing key leaves the record untouched
54
+ * const counts: Record<string, number> = { a: 1, b: 2 }
55
+ * assert.deepStrictEqual(
56
+ * RecordX.modifyIfExists(counts, "missing", (n) => n + 1),
57
+ * { a: 1, b: 2 },
58
+ * )
59
+ *
60
+ * // data-last (pipeable)
61
+ * assert.deepStrictEqual(
62
+ * pipe({ a: 1, b: 2 }, RecordX.modifyIfExists("a", (n) => n * 10)),
63
+ * { a: 10, b: 2 },
64
+ * )
65
+ * ```
66
+ *
67
+ * @category mapping
68
+ * @since 0.0.0
69
+ */
70
+ export const modifyIfExists: {
71
+ <K extends string, A>(
72
+ key: NoInfer<K>,
73
+ f: (a: A) => A,
74
+ ): (self: Record<K, A>) => Record<K, A>;
75
+ <K extends string, A>(
76
+ self: Record<K, A>,
77
+ key: NoInfer<K>,
78
+ f: (a: A) => A,
79
+ ): Record<K, A>;
80
+ } = dual(
81
+ 3,
82
+ <K extends string, A>(
83
+ self: Record<K, A>,
84
+ key: K,
85
+ f: (a: A) => A,
86
+ ): Record<K, A> =>
87
+ pipe(
88
+ Record.modify(self, key, f),
89
+ Option.getOrElse(() => self),
90
+ ),
91
+ );
92
+
93
+ /**
94
+ * Returns the smallest value of `record` (per `order`) that matches
95
+ * `predicate`, narrowed to the refined type `B`, or `Option.none()` if none
96
+ * match.
97
+ *
98
+ * The `Record` counterpart of `ArrayX.takeFirstWhere`: it considers only the
99
+ * record's values, keeps those satisfying `predicate`, and returns the minimum
100
+ * of them by `order`. Keys are ignored entirely.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * import { Option, Order, pipe } from "effect"
105
+ * import { RecordX } from "@nunofyobiz/effect-extras"
106
+ *
107
+ * const isEven = (n: number): n is number => n % 2 === 0
108
+ *
109
+ * // data-first
110
+ * assert.deepStrictEqual(
111
+ * RecordX.takeFirstWhere({ a: 3, b: 4, c: 2 }, isEven, Order.Number),
112
+ * Option.some(2),
113
+ * )
114
+ *
115
+ * // data-last (pipeable); no even values yields None
116
+ * assert.deepStrictEqual(
117
+ * pipe({ a: 1, b: 3 }, RecordX.takeFirstWhere(isEven, Order.Number)),
118
+ * Option.none(),
119
+ * )
120
+ * ```
121
+ *
122
+ * @category getters
123
+ * @since 0.0.0
124
+ */
125
+ export const takeFirstWhere = dual<
126
+ <K extends PropertyKey, A, B extends A>(
127
+ predicate: Predicate.Refinement<A, B>,
128
+ order: Order.Order<B>,
129
+ ) => (record: Record<K, A>) => Option.Option<B>,
130
+ <K extends PropertyKey, A, B extends A>(
131
+ record: Record<K, A>,
132
+ predicate: Predicate.Refinement<A, B>,
133
+ order: Order.Order<B>,
134
+ ) => Option.Option<B>
135
+ >(
136
+ 3,
137
+ <K extends PropertyKey, A, B extends A>(
138
+ record: Record<K, A>,
139
+ predicate: Predicate.Refinement<A, B>,
140
+ order: Order.Order<B>,
141
+ ): Option.Option<B> =>
142
+ ArrayX.takeFirstWhere(Record.values(record), predicate, order),
143
+ );
144
+
145
+ /**
146
+ * Returns the largest value of `record` (per `order`) that matches `predicate`,
147
+ * narrowed to the refined type `B`, or `Option.none()` if none match.
148
+ *
149
+ * The mirror of {@link takeFirstWhere}: it considers only the record's values,
150
+ * keeps those satisfying `predicate`, and returns the maximum of them by
151
+ * `order`. Keys are ignored entirely.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * import { Option, Order, pipe } from "effect"
156
+ * import { RecordX } from "@nunofyobiz/effect-extras"
157
+ *
158
+ * const isEven = (n: number): n is number => n % 2 === 0
159
+ *
160
+ * // data-first
161
+ * assert.deepStrictEqual(
162
+ * RecordX.takeLastWhere({ a: 3, b: 4, c: 2 }, isEven, Order.Number),
163
+ * Option.some(4),
164
+ * )
165
+ *
166
+ * // data-last (pipeable); no even values yields None
167
+ * assert.deepStrictEqual(
168
+ * pipe({ a: 1, b: 3 }, RecordX.takeLastWhere(isEven, Order.Number)),
169
+ * Option.none(),
170
+ * )
171
+ * ```
172
+ *
173
+ * @category getters
174
+ * @since 0.0.0
175
+ */
176
+ export const takeLastWhere = dual<
177
+ <K extends PropertyKey, A, B extends A>(
178
+ predicate: Predicate.Refinement<A, B>,
179
+ order: Order.Order<B>,
180
+ ) => (record: Record<K, A>) => Option.Option<B>,
181
+ <K extends PropertyKey, A, B extends A>(
182
+ record: Record<K, A>,
183
+ predicate: Predicate.Refinement<A, B>,
184
+ order: Order.Order<B>,
185
+ ) => Option.Option<B>
186
+ >(
187
+ 3,
188
+ <K extends PropertyKey, A, B extends A>(
189
+ record: Record<K, A>,
190
+ predicate: Predicate.Refinement<A, B>,
191
+ order: Order.Order<B>,
192
+ ): Option.Option<B> =>
193
+ ArrayX.takeLastWhere(Record.values(record), predicate, order),
194
+ );
195
+
196
+ /**
197
+ * Returns the largest value of `record` according to `order`, wrapped in an
198
+ * `Option` so empty records are handled safely.
199
+ *
200
+ * Sorts the record's values by `order` and takes the last one, yielding
201
+ * `Option.none()` when the record is empty and `Option.some(max)` otherwise.
202
+ * Keys are ignored — only values participate in the ordering.
203
+ *
204
+ * @example
205
+ * ```ts
206
+ * import { Option, Order, pipe } from "effect"
207
+ * import { RecordX } from "@nunofyobiz/effect-extras"
208
+ *
209
+ * // data-first
210
+ * assert.deepStrictEqual(
211
+ * RecordX.takeLast({ a: 3, b: 5, c: 1 }, Order.Number),
212
+ * Option.some(5),
213
+ * )
214
+ *
215
+ * // data-last (pipeable); empty record yields None
216
+ * assert.deepStrictEqual(
217
+ * pipe({}, RecordX.takeLast(Order.Number)),
218
+ * Option.none(),
219
+ * )
220
+ * ```
221
+ *
222
+ * @category getters
223
+ * @since 0.0.0
224
+ */
225
+ export const takeLast = dual<
226
+ <K extends PropertyKey, A>(
227
+ order: Order.Order<A>,
228
+ ) => (record: Record<K, A>) => Option.Option<A>,
229
+ <K extends PropertyKey, A>(
230
+ record: Record<K, A>,
231
+ order: Order.Order<A>,
232
+ ) => Option.Option<A>
233
+ >(
234
+ 2,
235
+ <K extends PropertyKey, A>(
236
+ record: Record<K, A>,
237
+ order: Order.Order<A>,
238
+ ): Option.Option<A> =>
239
+ pipe(Record.values(record), Array.sort(order), Array.last),
240
+ );
241
+
242
+ /**
243
+ * Re-types the keys of a `Record` to a different key type `K2` without touching
244
+ * the runtime value.
245
+ *
246
+ * A purely type-level reinterpretation: the record is returned as-is, but the
247
+ * compiler now treats its keys as `K2` instead of the inferred `K1`. Use it at a
248
+ * boundary where you know the keys conform to a narrower branded or literal key
249
+ * type that TypeScript can't infer from the value alone. It performs no
250
+ * validation — the caller is responsible for the keys actually matching `K2`.
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * import { RecordX } from "@nunofyobiz/effect-extras"
255
+ *
256
+ * type UserId = string & { readonly _brand: "UserId" }
257
+ *
258
+ * const byId: Record<string, number> = { u1: 10, u2: 20 }
259
+ * const branded = RecordX.keysAs<UserId>()(byId)
260
+ *
261
+ * // Same runtime value, keys now seen as `UserId`
262
+ * assert.deepStrictEqual(branded, { u1: 10, u2: 20 })
263
+ * ```
264
+ *
265
+ * @category conversions
266
+ * @since 0.0.0
267
+ */
268
+ export const keysAs =
269
+ <K2 extends PropertyKey>() =>
270
+ <K1 extends PropertyKey, V>(record: Record<K1, V>): Record<K2, V> =>
271
+ record as unknown as Record<K2, V>;
272
+
273
+ /**
274
+ * Returns the value at `key` in `record`, throwing an `Error` if the key is
275
+ * absent.
276
+ *
277
+ * The unsafe, get-or-explode counterpart to `Record.get` (which returns an
278
+ * `Option`). The thrown error names the missing key and lists the record's
279
+ * existing keys to aid debugging. Reach for {@link getOrThrowWith} when you need
280
+ * a custom error; prefer `Record.get` whenever absence is a real possibility.
281
+ *
282
+ * @example
283
+ * ```ts
284
+ * import { pipe } from "effect"
285
+ * import { RecordX } from "@nunofyobiz/effect-extras"
286
+ *
287
+ * const record: Record<string, number> = { a: 1, b: 2 }
288
+ *
289
+ * // data-first
290
+ * assert.deepStrictEqual(RecordX.getOrThrow(record, "a"), 1)
291
+ *
292
+ * // data-last (pipeable)
293
+ * assert.deepStrictEqual(pipe(record, RecordX.getOrThrow("b")), 2)
294
+ *
295
+ * // missing key throws
296
+ * assert.throws(() => RecordX.getOrThrow(record, "missing"))
297
+ * ```
298
+ *
299
+ * @category unsafe
300
+ * @since 0.0.0
301
+ */
302
+ export const getOrThrow = dual<
303
+ <K extends string | symbol>(key: K) => <V>(record: Record<K, V>) => V,
304
+ <K extends string | symbol, V>(record: Record<K, V>, key: K) => V
305
+ >(
306
+ 2,
307
+ <K extends string | symbol, V>(record: Record<K, V>, key: K): V =>
308
+ getOrThrowWith(
309
+ record,
310
+ key,
311
+ (key) =>
312
+ new Error(
313
+ `Key ${String(key)} not found in record\nExisting keys=${String(Record.keys(record))}`,
314
+ ),
315
+ ),
316
+ );
317
+
318
+ /**
319
+ * Returns the value at `key` in `record`, throwing the result of `onNone(key)`
320
+ * if the key is absent.
321
+ *
322
+ * Like {@link getOrThrow}, but lets the caller supply the thrown value (an
323
+ * `Error`, a string, or any custom failure) computed from the missing key. Use
324
+ * it when the default "key not found" message isn't descriptive enough for the
325
+ * call site.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * import { pipe } from "effect"
330
+ * import { RecordX } from "@nunofyobiz/effect-extras"
331
+ *
332
+ * const record: Record<string, number> = { a: 1 }
333
+ * const onNone = (key: string) => new Error(`no ${key}`)
334
+ *
335
+ * // data-first
336
+ * assert.deepStrictEqual(RecordX.getOrThrowWith(record, "a", onNone), 1)
337
+ *
338
+ * // data-last (pipeable)
339
+ * assert.deepStrictEqual(pipe(record, RecordX.getOrThrowWith("a", onNone)), 1)
340
+ *
341
+ * // missing key throws the custom error
342
+ * assert.throws(() => RecordX.getOrThrowWith(record, "missing", onNone))
343
+ * ```
344
+ *
345
+ * @category unsafe
346
+ * @since 0.0.0
347
+ */
348
+ export const getOrThrowWith = dual<
349
+ <K extends string | symbol>(
350
+ key: K,
351
+ onNone: (key: K) => unknown,
352
+ ) => <V>(record: Record<K, V>) => V,
353
+ <K extends string | symbol, V>(
354
+ record: Record<K, V>,
355
+ key: K,
356
+ onNone: (key: K) => unknown,
357
+ ) => V
358
+ >(
359
+ 3,
360
+ <K extends string | symbol, V>(
361
+ record: Record<K, V>,
362
+ key: K,
363
+ onNone: (key: K) => unknown,
364
+ ): V =>
365
+ Record.get(record, key).pipe(Option.getOrThrowWith(() => onNone(key))),
366
+ );
367
+
368
+ /**
369
+ * Inserts or updates the value at `key`, deriving the new value from the current
370
+ * one via `upsert`.
371
+ *
372
+ * The `upsert` function receives an `Option` of the existing value —
373
+ * `Option.some(value)` when the key is present, `Option.none()` when it's
374
+ * absent — and returns the value to store. This unifies "insert if missing" and
375
+ * "update if present" into a single pass, with `Option.match` as the natural way
376
+ * to handle both cases.
377
+ *
378
+ * @example
379
+ * ```ts
380
+ * import { Option, pipe } from "effect"
381
+ * import { RecordX } from "@nunofyobiz/effect-extras"
382
+ *
383
+ * const bump = Option.match({
384
+ * onNone: () => 1,
385
+ * onSome: (n: number) => n + 1,
386
+ * })
387
+ *
388
+ * // data-first: updates an existing entry
389
+ * assert.deepStrictEqual(RecordX.upsert({ a: 1 }, "a", bump), { a: 2 })
390
+ *
391
+ * // data-last (pipeable): inserts a missing entry
392
+ * const counts: Record<string, number> = { a: 1 }
393
+ * assert.deepStrictEqual(pipe(counts, RecordX.upsert("b", bump)), {
394
+ * a: 1,
395
+ * b: 1,
396
+ * })
397
+ * ```
398
+ *
399
+ * @category mapping
400
+ * @since 0.0.0
401
+ */
402
+ export const upsert = dual<
403
+ <K extends string | symbol, V>(
404
+ key: K,
405
+ upsert: (existingValue: Option.Option<V>) => V,
406
+ ) => (record: Record<K, V>) => Record<K, V>,
407
+ <K extends string | symbol, V>(
408
+ record: Record<K, V>,
409
+ key: K,
410
+ upsert: (existingValue: Option.Option<V>) => V,
411
+ ) => Record<K, V>
412
+ >(
413
+ 3,
414
+ <K extends string | symbol, V>(
415
+ record: Record<K, V>,
416
+ key: K,
417
+ upsert: (existingValue: Option.Option<V>) => V,
418
+ ): Record<K, V> => {
419
+ const existingValue = Record.get(record, key);
420
+ const updatedValue = upsert(existingValue);
421
+ return Record.set(record, key, updatedValue);
422
+ },
423
+ );
424
+
425
+ /**
426
+ * Indexes an iterable of values into a `Record`, keying each value by the result
427
+ * of `identify`.
428
+ *
429
+ * Builds a lookup table from a collection: every value is stored under the key
430
+ * `identify(value)`. When two values produce the same key the later one wins, so
431
+ * the result holds the last value seen per key. Useful for turning a list of
432
+ * records into a by-id map.
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * import { pipe } from "effect"
437
+ * import { RecordX } from "@nunofyobiz/effect-extras"
438
+ *
439
+ * const items = [
440
+ * { id: "a", n: 1 },
441
+ * { id: "b", n: 2 },
442
+ * { id: "a", n: 3 }, // wins over the earlier "a"
443
+ * ]
444
+ *
445
+ * // data-first
446
+ * assert.deepStrictEqual(
447
+ * RecordX.collectBy(items, (item) => item.id),
448
+ * { a: { id: "a", n: 3 }, b: { id: "b", n: 2 } },
449
+ * )
450
+ *
451
+ * // data-last (pipeable)
452
+ * assert.deepStrictEqual(
453
+ * pipe(items, RecordX.collectBy((item) => item.id)),
454
+ * { a: { id: "a", n: 3 }, b: { id: "b", n: 2 } },
455
+ * )
456
+ * ```
457
+ *
458
+ * @category constructors
459
+ * @since 0.0.0
460
+ */
461
+ export const collectBy = dual<
462
+ <K extends string | symbol, V>(
463
+ identify: (v: V) => K,
464
+ ) => (values: Iterable<V>) => Record<K, V>,
465
+ <K extends string | symbol, V>(
466
+ values: Iterable<V>,
467
+ identify: (v: V) => K,
468
+ ) => Record<K, V>
469
+ >(
470
+ 2,
471
+ <K extends string | symbol, V>(
472
+ values: Iterable<V>,
473
+ identify: (v: V) => K,
474
+ ): Record<K, V> =>
475
+ Array.reduce(values, {} as Record<K, V>, (accumulator, value) =>
476
+ Record.set<K, V, K, V>(accumulator, identify(value), value),
477
+ ),
478
+ );
@@ -0,0 +1 @@
1
+ export * as RecordX from "./RecordX";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Result` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Option, Result } from "effect";
7
+
8
+ /**
9
+ * Lifts an `Option` into a `Result` with a `void` failure: `Some(value)` becomes
10
+ * `Result.succeed(value)` and `None` becomes `Result.failVoid`.
11
+ *
12
+ * Useful in v4 where `Array.filterMap` and `Record.filterMap` expect
13
+ * `Result`-returning predicates (a `Success` keeps the value, a `Failure` drops
14
+ * it); in v3 those APIs accepted `Option`-returning predicates directly:
15
+ *
16
+ * ```ts
17
+ * import { Array } from "effect"
18
+ * import { ResultX } from "@nunofyobiz/effect-extras"
19
+ *
20
+ * declare const items: ReadonlyArray<number>
21
+ * declare const maybeTransform: (item: number) => import("effect").Option.Option<string>
22
+ *
23
+ * // v3: Array.filterMap(items, (item) => maybeTransform(item))
24
+ * // v4:
25
+ * Array.filterMap(items, (item) => ResultX.fromOption(maybeTransform(item)))
26
+ * ```
27
+ *
28
+ * Effect ships `Result.fromOption(option, onNone)` which requires a non-`void`
29
+ * failure value; this helper specializes to the common "drop the item, no error
30
+ * needed" case used by `filterMap`.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { Option, Result } from "effect"
35
+ * import { ResultX } from "@nunofyobiz/effect-extras"
36
+ *
37
+ * assert.deepStrictEqual(
38
+ * ResultX.fromOption(Option.some(1)),
39
+ * Result.succeed(1),
40
+ * )
41
+ * assert.deepStrictEqual(
42
+ * ResultX.fromOption(Option.none<number>()),
43
+ * Result.failVoid,
44
+ * )
45
+ * ```
46
+ *
47
+ * @category conversions
48
+ * @since 0.0.0
49
+ */
50
+ export const fromOption = <A>(
51
+ option: Option.Option<A>,
52
+ ): Result.Result<A, void> =>
53
+ Option.isSome(option) ? Result.succeed(option.value) : Result.failVoid;
@@ -0,0 +1 @@
1
+ export * as ResultX from "./ResultX";