@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,234 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Option` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Option, Predicate, pipe } from "effect";
7
+ import { dual } from "effect/Function";
8
+
9
+ /**
10
+ * Combines two `Option`s into an `Option` of a tuple, succeeding only when both
11
+ * are `Some`.
12
+ *
13
+ * Returns `Some([a, b])` when both inputs are `Some`, and `None` if either is
14
+ * `None`. Useful when an operation needs two optional values present at once.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { Option } from "effect"
19
+ * import { OptionX } from "@nunofyobiz/effect-extras"
20
+ *
21
+ * // Both Some — succeeds with the pair
22
+ * assert.deepStrictEqual(
23
+ * OptionX.tupleOf(Option.some(1), Option.some("a")),
24
+ * Option.some([1, "a"]),
25
+ * )
26
+ *
27
+ * // Either None collapses to None
28
+ * assert.deepStrictEqual(
29
+ * OptionX.tupleOf(Option.some(1), Option.none()),
30
+ * Option.none(),
31
+ * )
32
+ * ```
33
+ *
34
+ * @category combinators
35
+ * @since 0.0.0
36
+ */
37
+ export const tupleOf = dual<
38
+ <A>(a: Option.Option<A>) => <B>(b: Option.Option<B>) => Option.Option<[A, B]>,
39
+ <A, B>(a: Option.Option<A>, b: Option.Option<B>) => Option.Option<[A, B]>
40
+ >(
41
+ 2,
42
+ <A, B>(a: Option.Option<A>, b: Option.Option<B>): Option.Option<[A, B]> =>
43
+ Option.flatMap(a, (a) => Option.map(b, (b) => [a, b])),
44
+ );
45
+
46
+ /**
47
+ * Runs a side effect with the value of an `Option` when it is `Some`, doing
48
+ * nothing when it is `None`.
49
+ *
50
+ * A shorthand for the "if Some, do something" branch of `Option.match` where the
51
+ * `None` case is a no-op. The callback's return value is ignored — `ifSome`
52
+ * always returns `void`.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { Option } from "effect"
57
+ * import { OptionX } from "@nunofyobiz/effect-extras"
58
+ *
59
+ * const log: Array<number> = []
60
+ * OptionX.ifSome(Option.some(1), (value) => log.push(value))
61
+ * OptionX.ifSome(Option.none<number>(), (value) => log.push(value))
62
+ *
63
+ * assert.deepStrictEqual(log, [1])
64
+ * ```
65
+ *
66
+ * @category sequencing
67
+ * @since 0.0.0
68
+ */
69
+ export const ifSome = dual<
70
+ <A>(ifSome: (value: A) => void) => (self: Option.Option<A>) => void,
71
+ <A>(self: Option.Option<A>, ifSome: (value: A) => void) => void
72
+ >(2, <A>(self: Option.Option<A>, ifSome: (value: A) => void): void => {
73
+ Option.match(self, {
74
+ onSome: (value) => {
75
+ ifSome(value);
76
+ // Don't return anything
77
+ },
78
+ onNone: () => {
79
+ // Do nothing
80
+ },
81
+ });
82
+ });
83
+
84
+ /**
85
+ * Runs a side effect with the value of an `Option` when it is `Some`, then
86
+ * returns the `Option` unchanged.
87
+ *
88
+ * The pass-through counterpart of {@link ifSome}: it taps into a `Some` value
89
+ * (for logging, metrics, debugging) without breaking a `pipe` chain, since it
90
+ * returns the original `Option`. For `None` it is a no-op.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * import { Option, pipe } from "effect"
95
+ * import { OptionX } from "@nunofyobiz/effect-extras"
96
+ *
97
+ * const log: Array<number> = []
98
+ * const result = pipe(
99
+ * Option.some(1),
100
+ * OptionX.inspectSome((value) => log.push(value)),
101
+ * Option.map((value) => value + 1),
102
+ * )
103
+ *
104
+ * assert.deepStrictEqual(result, Option.some(2))
105
+ * assert.deepStrictEqual(log, [1])
106
+ * ```
107
+ *
108
+ * @category sequencing
109
+ * @since 0.0.0
110
+ */
111
+ export const inspectSome = dual<
112
+ <A>(
113
+ function_: (value: A) => void,
114
+ ) => (self: Option.Option<A>) => Option.Option<A>,
115
+ <A>(self: Option.Option<A>, function_: (value: A) => void) => Option.Option<A>
116
+ >(
117
+ 2,
118
+ <A>(
119
+ self: Option.Option<A>,
120
+ function_: (value: A) => void,
121
+ ): Option.Option<A> => {
122
+ ifSome(self, function_);
123
+ return self;
124
+ },
125
+ );
126
+
127
+ /**
128
+ * Normalizes a possibly-nullish `Option` into a plain `Option`, mapping `null`
129
+ * and `undefined` to `None`.
130
+ *
131
+ * Handy at boundaries where an `Option` value might itself arrive as `null` or
132
+ * `undefined` (for example an optional field that holds an `Option`): the result
133
+ * is always a well-formed `Option`, never `null`/`undefined`.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * import { Option } from "effect"
138
+ * import { OptionX } from "@nunofyobiz/effect-extras"
139
+ *
140
+ * assert.deepStrictEqual(OptionX.fromNullableOption(Option.some(1)), Option.some(1))
141
+ * assert.deepStrictEqual(OptionX.fromNullableOption(null), Option.none())
142
+ * assert.deepStrictEqual(OptionX.fromNullableOption(undefined), Option.none())
143
+ * ```
144
+ *
145
+ * @category constructors
146
+ * @since 0.0.0
147
+ */
148
+ export const fromNullableOption = <A>(
149
+ nullableOption: Option.Option<A> | null | undefined,
150
+ ): Option.Option<A> =>
151
+ Predicate.isNotNullish(nullableOption) ? nullableOption : Option.none();
152
+
153
+ /**
154
+ * Maps the value of an `Option` when it is `Some`, returning `null` when it is
155
+ * `None`.
156
+ *
157
+ * A shorthand for `pipe(self, Option.map(map), Option.getOrNull)`. The `null`
158
+ * fallback makes it especially convenient in JSX/React, where rendering `null`
159
+ * skips output — `mapSomeOrNull(value, (v) => render(v))` replaces a more verbose
160
+ * `Option.match` with `onNone: () => null`.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * import { Option, pipe } from "effect"
165
+ * import { OptionX } from "@nunofyobiz/effect-extras"
166
+ *
167
+ * // data-first
168
+ * assert.deepStrictEqual(OptionX.mapSomeOrNull(Option.some(1), (v) => v + 1), 2)
169
+ *
170
+ * // None maps to null
171
+ * assert.deepStrictEqual(
172
+ * OptionX.mapSomeOrNull(Option.none<number>(), (v) => v + 1),
173
+ * null,
174
+ * )
175
+ *
176
+ * // data-last (piped)
177
+ * assert.deepStrictEqual(
178
+ * pipe(Option.some(1), OptionX.mapSomeOrNull((v) => v + 1)),
179
+ * 2,
180
+ * )
181
+ * ```
182
+ *
183
+ * @category mapping
184
+ * @since 0.0.0
185
+ */
186
+ export const mapSomeOrNull = dual<
187
+ <A, B>(map: (a: A) => B) => (self: Option.Option<A>) => B | null,
188
+ <A, B>(self: Option.Option<A>, map: (a: A) => B) => B | null
189
+ >(2, <A, B>(self: Option.Option<A>, map: (a: A) => B): B | null =>
190
+ pipe(self, Option.map(map), Option.getOrNull),
191
+ );
192
+
193
+ /**
194
+ * Maps the value of an `Option` when it is `Some`, returning `undefined` when it
195
+ * is `None`.
196
+ *
197
+ * The `undefined`-returning counterpart of {@link mapSomeOrNull}: a shorthand for
198
+ * `pipe(self, Option.map(map), Option.getOrUndefined)`. Reach for it when the
199
+ * consuming API expects `undefined` rather than `null` for "absent" (for example
200
+ * an optional prop or a value spread into an object).
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * import { Option, pipe } from "effect"
205
+ * import { OptionX } from "@nunofyobiz/effect-extras"
206
+ *
207
+ * // data-first
208
+ * assert.deepStrictEqual(
209
+ * OptionX.mapSomeOrUndefined(Option.some(1), (v) => v + 1),
210
+ * 2,
211
+ * )
212
+ *
213
+ * // None maps to undefined
214
+ * assert.deepStrictEqual(
215
+ * OptionX.mapSomeOrUndefined(Option.none<number>(), (v) => v + 1),
216
+ * undefined,
217
+ * )
218
+ *
219
+ * // data-last (piped)
220
+ * assert.deepStrictEqual(
221
+ * pipe(Option.some(1), OptionX.mapSomeOrUndefined((v) => v + 1)),
222
+ * 2,
223
+ * )
224
+ * ```
225
+ *
226
+ * @category mapping
227
+ * @since 0.0.0
228
+ */
229
+ export const mapSomeOrUndefined = dual<
230
+ <A, B>(map: (a: A) => B) => (self: Option.Option<A>) => B | undefined,
231
+ <A, B>(self: Option.Option<A>, map: (a: A) => B) => B | undefined
232
+ >(2, <A, B>(self: Option.Option<A>, map: (a: A) => B): B | undefined =>
233
+ pipe(self, Option.map(map), Option.getOrUndefined),
234
+ );
@@ -0,0 +1 @@
1
+ export * as OptionX from "./OptionX";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Order` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Number as EffectNumber, Order } from "effect";
7
+
8
+ /**
9
+ * Builds an `Order.Order` for an enum-like set of values from an explicit rank
10
+ * table, sorting each value by its assigned numeric rank.
11
+ *
12
+ * Use it when a union of string (or other `PropertyKey`) literals has a natural
13
+ * priority that isn't its alphabetical order — pass a record mapping every
14
+ * member to a rank and the resulting order sorts ascending by that rank.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { OrderX } from "@nunofyobiz/effect-extras"
19
+ * import { Array } from "effect"
20
+ *
21
+ * const byAge = OrderX.rankedEnum({ child: 0, parent: 1, grandparent: 2 })
22
+ *
23
+ * assert.deepStrictEqual(
24
+ * Array.sort(["parent", "grandparent", "child"], byAge),
25
+ * ["child", "parent", "grandparent"]
26
+ * )
27
+ * ```
28
+ *
29
+ * @category ordering
30
+ * @since 0.0.0
31
+ */
32
+ export const rankedEnum =
33
+ <const A extends PropertyKey>(ranks: Record<A, number>): Order.Order<A> =>
34
+ (self, that) =>
35
+ EffectNumber.sign(ranks[self] - ranks[that]);
@@ -0,0 +1 @@
1
+ export * as OrderX from "./OrderX";
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Predicate` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Predicate, String } from "effect";
7
+ import { dual } from "effect/Function";
8
+
9
+ /**
10
+ * Runs a refinement against `value` and branches on the result, passing the
11
+ * narrowed value to the `whenTrue` handler.
12
+ *
13
+ * It pairs a `Predicate.Refinement<A, B>` with two continuations so the success
14
+ * branch receives `value` already narrowed to `B`, replacing an `if`/`else` that
15
+ * would otherwise re-check or cast. Supports both data-first and data-last
16
+ * (pipeable) call styles.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { PredicateX } from "@nunofyobiz/effect-extras"
21
+ * import { Predicate, pipe } from "effect"
22
+ *
23
+ * // Data-first
24
+ * assert.deepStrictEqual(
25
+ * PredicateX.matchRefine("hello", Predicate.isString, {
26
+ * whenTrue: (value) => `String: ${value}`,
27
+ * whenFalse: () => "Not a string"
28
+ * }),
29
+ * "String: hello"
30
+ * )
31
+ *
32
+ * // Data-last (pipeable)
33
+ * assert.deepStrictEqual(
34
+ * pipe(
35
+ * 42,
36
+ * PredicateX.matchRefine(Predicate.isString, {
37
+ * whenTrue: (value) => `String: ${value}`,
38
+ * whenFalse: () => "Not a string"
39
+ * })
40
+ * ),
41
+ * "Not a string"
42
+ * )
43
+ * ```
44
+ *
45
+ * @category pattern matching
46
+ * @since 0.0.0
47
+ */
48
+ export const matchRefine = dual<
49
+ <A, B extends A, C>(
50
+ predicate: Predicate.Refinement<A, B>,
51
+ handlers: {
52
+ whenFalse: () => C;
53
+ whenTrue: (value: B) => C;
54
+ },
55
+ ) => (value: A) => C,
56
+ <A, B extends A, C>(
57
+ value: A,
58
+ predicate: Predicate.Refinement<A, B>,
59
+ handlers: { whenFalse: () => C; whenTrue: (value: B) => C },
60
+ ) => C
61
+ >(
62
+ 3,
63
+ <A, B extends A, C>(
64
+ value: A,
65
+ predicate: Predicate.Refinement<A, B>,
66
+ handlers: { whenFalse: () => C; whenTrue: (value: B) => C },
67
+ ): C => (predicate(value) ? handlers.whenTrue(value) : handlers.whenFalse()),
68
+ );
69
+
70
+ /**
71
+ * Refines an `unknown` value to a non-empty `string`, returning `true` only when
72
+ * the value is present, is a `string`, and has at least one character.
73
+ *
74
+ * This is the compound guard the repo's conventions call for: it folds
75
+ * `Predicate.isNotNullish`, `Predicate.isString`, and `String.isNonEmpty` into a
76
+ * single reusable refinement so call sites get `value is string` narrowing
77
+ * without restating the three checks.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * import { PredicateX } from "@nunofyobiz/effect-extras"
82
+ *
83
+ * assert.deepStrictEqual(PredicateX.isNonEmptyString("hello"), true)
84
+ * assert.deepStrictEqual(PredicateX.isNonEmptyString(""), false)
85
+ * assert.deepStrictEqual(PredicateX.isNonEmptyString(null), false)
86
+ * assert.deepStrictEqual(PredicateX.isNonEmptyString(123), false)
87
+ * ```
88
+ *
89
+ * @category refinements
90
+ * @since 0.0.0
91
+ */
92
+ export function isNonEmptyString(value: unknown): value is string {
93
+ return (
94
+ Predicate.isNotNullish(value) &&
95
+ Predicate.isString(value) &&
96
+ String.isNonEmpty(value)
97
+ );
98
+ }
@@ -0,0 +1 @@
1
+ export * as PredicateX from "./PredicateX";
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Helpers for working with native `Promise`s.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ /**
7
+ * Discards the resolved value of a `Promise`, returning a `Promise<void>` that
8
+ * resolves once the original settles.
9
+ *
10
+ * Useful when you await a promise only for its side effect and want the result
11
+ * type to reflect that nothing meaningful is produced — for example when an API
12
+ * expects a `Promise<void>` or when narrowing a value-bearing promise to keep
13
+ * call sites from accidentally depending on the resolved value. A rejection of
14
+ * the original promise still propagates.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { PromiseX } from "@nunofyobiz/effect-extras"
19
+ *
20
+ * const run = async (): Promise<void> => {
21
+ * const result = await PromiseX.asVoid(Promise.resolve(42))
22
+ * assert.deepStrictEqual(result, undefined)
23
+ * }
24
+ *
25
+ * void run()
26
+ * ```
27
+ *
28
+ * @category conversions
29
+ * @since 0.0.0
30
+ */
31
+ export const asVoid = <T>(promise: Promise<T>): Promise<void> =>
32
+ promise.then(() => undefined);
@@ -0,0 +1 @@
1
+ export * as PromiseX from "./PromiseX";