@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.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +3703 -0
- package/dist/index.js +1006 -0
- package/dist/index.js.map +1 -0
- package/package.json +103 -0
- package/src/ArrayX/ArrayX.ts +818 -0
- package/src/ArrayX/index.ts +1 -0
- package/src/BigIntX/BigIntX.ts +35 -0
- package/src/BigIntX/index.ts +1 -0
- package/src/BooleanX/BooleanX.ts +24 -0
- package/src/BooleanX/index.ts +1 -0
- package/src/DurationX/DurationX.ts +178 -0
- package/src/DurationX/index.ts +1 -0
- package/src/EffectX/EffectX.ts +183 -0
- package/src/EffectX/index.ts +1 -0
- package/src/FormDataX/FormDataX.ts +57 -0
- package/src/FormDataX/index.ts +1 -0
- package/src/MapX/MapX.ts +54 -0
- package/src/MapX/index.ts +1 -0
- package/src/NonNullableX/NonNullableX.ts +290 -0
- package/src/NonNullableX/index.ts +2 -0
- package/src/NumberX/NumberX.ts +282 -0
- package/src/NumberX/index.ts +1 -0
- package/src/OptionX/OptionX.ts +234 -0
- package/src/OptionX/index.ts +1 -0
- package/src/OrderX/OrderX.ts +35 -0
- package/src/OrderX/index.ts +1 -0
- package/src/PredicateX/PredicateX.ts +98 -0
- package/src/PredicateX/index.ts +1 -0
- package/src/PromiseX/PromiseX.ts +32 -0
- package/src/PromiseX/index.ts +1 -0
- package/src/RecordX/RecordX.ts +478 -0
- package/src/RecordX/index.ts +1 -0
- package/src/ResultX/ResultX.ts +53 -0
- package/src/ResultX/index.ts +1 -0
- package/src/SchemaX/SchemaX.ts +324 -0
- package/src/SchemaX/index.ts +1 -0
- package/src/SetX/SetX.ts +160 -0
- package/src/SetX/index.ts +1 -0
- package/src/StringX/StringX.ts +97 -0
- package/src/StringX/index.ts +1 -0
- package/src/StructX/StructX.ts +310 -0
- package/src/StructX/index.ts +1 -0
- package/src/These/These.ts +1173 -0
- package/src/These/index.ts +1 -0
- package/src/index.ts +20 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic, framework-agnostic extensions to Effect's `Struct` module.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.0.0
|
|
5
|
+
*/
|
|
6
|
+
import { Option, Predicate, Record, pipe } from "effect";
|
|
7
|
+
import { dual } from "effect/Function";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Describes the shape of a per-field transformation over an object `O`: a record
|
|
11
|
+
* `T` whose values are functions taking the corresponding field of `O`.
|
|
12
|
+
*
|
|
13
|
+
* Used to type the "evolve" pattern, where each provided key maps to a function
|
|
14
|
+
* that receives that field's current value. Copied from Effect's internal
|
|
15
|
+
* `Struct.evolve()` type, which is not exported — it would be better to use
|
|
16
|
+
* theirs directly if it ever becomes public.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { StructX } from "@nunofyobiz/effect-extras"
|
|
21
|
+
*
|
|
22
|
+
* type Transform = StructX.PartialTransform<
|
|
23
|
+
* { a: number; b: string },
|
|
24
|
+
* { a: (n: number) => boolean }
|
|
25
|
+
* >
|
|
26
|
+
*
|
|
27
|
+
* const evolve: Transform = { a: (n) => n > 0 }
|
|
28
|
+
*
|
|
29
|
+
* assert.deepStrictEqual(evolve.a(1), true)
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @category models
|
|
33
|
+
* @since 0.0.0
|
|
34
|
+
*/
|
|
35
|
+
export type PartialTransform<O, T> = {
|
|
36
|
+
[K in keyof T]: T[K] extends (a: O[K & keyof O]) => unknown
|
|
37
|
+
? T[K]
|
|
38
|
+
: (a: O[K & keyof O]) => unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Builds a singleton record `{ [name]: value }` when `value` is defined, or an
|
|
43
|
+
* empty object `{}` when it is `undefined` — meant to be spread into an object
|
|
44
|
+
* literal.
|
|
45
|
+
*
|
|
46
|
+
* This is the canonical fix for `exactOptionalPropertyTypes: true` (which this
|
|
47
|
+
* repo enables for Effect Schema). Under that flag, spreading
|
|
48
|
+
* `{ key: maybeUndefined }` into an object whose key is `key?: T` is a type
|
|
49
|
+
* error: the property must be *absent*, not present-but-`undefined`. Spreading
|
|
50
|
+
* `...defined("key", maybeUndefined)` instead conditionally omits the property
|
|
51
|
+
* altogether. Note that `null` and other falsy-but-defined values (`0`, `""`,
|
|
52
|
+
* `false`) are kept — only `undefined` is dropped.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { StructX } from "@nunofyobiz/effect-extras"
|
|
57
|
+
*
|
|
58
|
+
* // Defined value → singleton record
|
|
59
|
+
* assert.deepStrictEqual(StructX.defined("name", "Ada"), { name: "Ada" })
|
|
60
|
+
*
|
|
61
|
+
* // undefined → property omitted entirely
|
|
62
|
+
* assert.deepStrictEqual(StructX.defined("name", undefined), {})
|
|
63
|
+
*
|
|
64
|
+
* // Falsy-but-defined values are kept
|
|
65
|
+
* assert.deepStrictEqual(StructX.defined("name", 0), { name: 0 })
|
|
66
|
+
*
|
|
67
|
+
* // Typical use: spread to conditionally include an optional field
|
|
68
|
+
* const maybeAge: number | undefined = undefined
|
|
69
|
+
* assert.deepStrictEqual({ id: 1, ...StructX.defined("age", maybeAge) }, {
|
|
70
|
+
* id: 1,
|
|
71
|
+
* })
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @category constructors
|
|
75
|
+
* @since 0.0.0
|
|
76
|
+
*/
|
|
77
|
+
export const defined = <const K extends string, V>(
|
|
78
|
+
name: K,
|
|
79
|
+
value: V | undefined,
|
|
80
|
+
): Partial<Record<K, Exclude<V, undefined>>> =>
|
|
81
|
+
Predicate.isUndefined(value)
|
|
82
|
+
? {}
|
|
83
|
+
: Record.singleton(name, value as Exclude<V, undefined>);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Removes every property whose value is `undefined` from `record`, narrowing
|
|
87
|
+
* each remaining property type to exclude `undefined`.
|
|
88
|
+
*
|
|
89
|
+
* The bulk counterpart of {@link defined}: instead of building one optional
|
|
90
|
+
* field, it filters a whole object. Very useful for update/patch actions where
|
|
91
|
+
* `undefined` means "leave this field unchanged" while every other value —
|
|
92
|
+
* including `null`, `0`, `""`, `false`, and `Option.none()` — means "set the
|
|
93
|
+
* field to exactly this".
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* import { StructX } from "@nunofyobiz/effect-extras"
|
|
98
|
+
*
|
|
99
|
+
* // Drops `b` (undefined) but keeps every other value, including null/0/false
|
|
100
|
+
* assert.deepStrictEqual(
|
|
101
|
+
* StructX.filterDefined({ a: "x", b: undefined, c: 0, d: null, e: false }),
|
|
102
|
+
* { a: "x", c: 0, d: null, e: false },
|
|
103
|
+
* )
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @category filtering
|
|
107
|
+
* @since 0.0.0
|
|
108
|
+
*/
|
|
109
|
+
export const filterDefined = <R extends Record<string, unknown>>(
|
|
110
|
+
record: R,
|
|
111
|
+
): Partial<{ [P in keyof R]: Exclude<R[P], undefined> }> =>
|
|
112
|
+
Object.entries(record).reduce(
|
|
113
|
+
(accumulator, [key, value]) => ({ ...accumulator, ...defined(key, value) }),
|
|
114
|
+
{} as Partial<{ [P in keyof R]: Exclude<R[P], undefined> }>,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Builds a singleton record `{ [name]: value }` when `value` is `Some`, or an
|
|
119
|
+
* empty object `{}` when it is `None` — meant to be spread into an object
|
|
120
|
+
* literal.
|
|
121
|
+
*
|
|
122
|
+
* The `Option`-valued sibling of {@link defined}: where `defined` keys off
|
|
123
|
+
* `undefined`, this keys off `Option` presence. Spread `...some("key", opt)` to
|
|
124
|
+
* conditionally include a field only when the `Option` carries a value, which
|
|
125
|
+
* stays correct under `exactOptionalPropertyTypes: true`.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* import { Option } from "effect"
|
|
130
|
+
* import { StructX } from "@nunofyobiz/effect-extras"
|
|
131
|
+
*
|
|
132
|
+
* // Some → singleton record
|
|
133
|
+
* assert.deepStrictEqual(StructX.some("name", Option.some("Ada")), {
|
|
134
|
+
* name: "Ada",
|
|
135
|
+
* })
|
|
136
|
+
*
|
|
137
|
+
* // None → property omitted entirely
|
|
138
|
+
* assert.deepStrictEqual(StructX.some("name", Option.none()), {})
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* @category constructors
|
|
142
|
+
* @since 0.0.0
|
|
143
|
+
*/
|
|
144
|
+
export const some = <const K extends string, V>(
|
|
145
|
+
name: K,
|
|
146
|
+
value: Option.Option<V>,
|
|
147
|
+
): Partial<Record<K, V>> =>
|
|
148
|
+
Option.match(value, {
|
|
149
|
+
onSome: (someValue) => Record.singleton(name, someValue),
|
|
150
|
+
onNone: () => ({}),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Like {@link some}, but with swapped argument order for piping. Returns a
|
|
155
|
+
* singleton record `{ [name]: value }` when the Option is `Some`, or an
|
|
156
|
+
* empty object `{}` when `None`.
|
|
157
|
+
*
|
|
158
|
+
* Internal-only — exists to support {@link pickSome} in data-last form.
|
|
159
|
+
* Not exported because the codebase has no direct callers.
|
|
160
|
+
*/
|
|
161
|
+
const someSingleton: {
|
|
162
|
+
<const K extends string>(
|
|
163
|
+
name: K,
|
|
164
|
+
): <V>(value: Option.Option<V>) => Partial<Record<K, V>>;
|
|
165
|
+
<V, const K extends string>(
|
|
166
|
+
value: Option.Option<V>,
|
|
167
|
+
name: K,
|
|
168
|
+
): Partial<Record<K, V>>;
|
|
169
|
+
} = dual(
|
|
170
|
+
2,
|
|
171
|
+
<V, const K extends string>(
|
|
172
|
+
value: Option.Option<V>,
|
|
173
|
+
name: K,
|
|
174
|
+
): Partial<Record<K, V>> => some(name, value),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Looks up `key` in a record of `Option` values and returns a singleton record
|
|
179
|
+
* when the value is `Some`, or `{}` when the key is absent or its value is
|
|
180
|
+
* `None`.
|
|
181
|
+
*
|
|
182
|
+
* Combines `Record.get` + `Option.flatten` + a singleton builder in one call —
|
|
183
|
+
* ideal for spreading an optional field out of a lookup table into an object
|
|
184
|
+
* literal under `exactOptionalPropertyTypes: true`. By default the output key
|
|
185
|
+
* matches the lookup key; pass `renameKeyTo` to emit the value under a different
|
|
186
|
+
* name.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* import { Option } from "effect"
|
|
191
|
+
* import { StructX } from "@nunofyobiz/effect-extras"
|
|
192
|
+
*
|
|
193
|
+
* const lookup = {
|
|
194
|
+
* a: Option.some(100),
|
|
195
|
+
* b: Option.none<number>(),
|
|
196
|
+
* }
|
|
197
|
+
*
|
|
198
|
+
* // Some → singleton record under the same key
|
|
199
|
+
* assert.deepStrictEqual(StructX.pickSome(lookup, "a"), { a: 100 })
|
|
200
|
+
*
|
|
201
|
+
* // None → property omitted entirely
|
|
202
|
+
* assert.deepStrictEqual(StructX.pickSome(lookup, "b"), {})
|
|
203
|
+
*
|
|
204
|
+
* // Rename the output key
|
|
205
|
+
* assert.deepStrictEqual(StructX.pickSome(lookup, "a", "count"), { count: 100 })
|
|
206
|
+
* ```
|
|
207
|
+
*
|
|
208
|
+
* @category constructors
|
|
209
|
+
* @since 0.0.0
|
|
210
|
+
*/
|
|
211
|
+
export function pickSome<const K extends string, V>(
|
|
212
|
+
record: Record<K, Option.Option<V>>,
|
|
213
|
+
key: K,
|
|
214
|
+
): Partial<Record<K, V>>;
|
|
215
|
+
export function pickSome<const K1 extends string, V, const K2 extends string>(
|
|
216
|
+
record: Record<K1, Option.Option<V>>,
|
|
217
|
+
key: K1,
|
|
218
|
+
renameKeyTo: K2,
|
|
219
|
+
): Partial<Record<K2, V>>;
|
|
220
|
+
export function pickSome<const K1 extends string, V, const K2 extends string>(
|
|
221
|
+
record: Record<K1, Option.Option<V>>,
|
|
222
|
+
key: K1,
|
|
223
|
+
renameKeyTo?: K2,
|
|
224
|
+
): Partial<Record<K1 | K2, V>> {
|
|
225
|
+
return pipe(
|
|
226
|
+
Record.get(record, key),
|
|
227
|
+
Option.flatten,
|
|
228
|
+
someSingleton(renameKeyTo ?? key),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Builds a singleton record `{ [name]: value }` when `value` is truthy, or an
|
|
234
|
+
* empty object `{}` otherwise — meant to be spread into an object literal.
|
|
235
|
+
*
|
|
236
|
+
* Stricter than {@link defined}: it drops not just `undefined` but every falsy
|
|
237
|
+
* value (`null`, `false`, `0`, `""`, `NaN`), and narrows the value type
|
|
238
|
+
* accordingly. Use it to spread a field only when it carries a meaningful,
|
|
239
|
+
* non-falsy value.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```ts
|
|
243
|
+
* import { StructX } from "@nunofyobiz/effect-extras"
|
|
244
|
+
*
|
|
245
|
+
* // Truthy value → singleton record
|
|
246
|
+
* assert.deepStrictEqual(StructX.truthy("name", "Ada"), { name: "Ada" })
|
|
247
|
+
*
|
|
248
|
+
* // Falsy values → property omitted entirely
|
|
249
|
+
* assert.deepStrictEqual(StructX.truthy("name", ""), {})
|
|
250
|
+
* assert.deepStrictEqual(StructX.truthy("name", 0), {})
|
|
251
|
+
* assert.deepStrictEqual(StructX.truthy("name", false), {})
|
|
252
|
+
* ```
|
|
253
|
+
*
|
|
254
|
+
* @category constructors
|
|
255
|
+
* @since 0.0.0
|
|
256
|
+
*/
|
|
257
|
+
export const truthy = <const K extends string, V>(
|
|
258
|
+
name: K,
|
|
259
|
+
value: V,
|
|
260
|
+
): Partial<Record<K, Exclude<NonNullable<V>, false | 0 | "">>> =>
|
|
261
|
+
Predicate.isTruthy(value)
|
|
262
|
+
? Record.singleton(name, value as Exclude<NonNullable<V>, false | 0 | "">)
|
|
263
|
+
: {};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Returns `true` when `object` has `key` set to a non-nullish value, narrowing
|
|
267
|
+
* that property to `NonNullable` on the type level.
|
|
268
|
+
*
|
|
269
|
+
* A type guard combining `hasProperty` with a nullish check: after a successful
|
|
270
|
+
* test, TypeScript treats `object[key]` as present and free of `null` /
|
|
271
|
+
* `undefined`. Handy as a predicate passed to `Array.filter` to keep only the
|
|
272
|
+
* elements whose `key` is populated.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* import { pipe } from "effect"
|
|
277
|
+
* import { StructX } from "@nunofyobiz/effect-extras"
|
|
278
|
+
*
|
|
279
|
+
* // data-first
|
|
280
|
+
* assert.deepStrictEqual(
|
|
281
|
+
* StructX.hasNotNullableProperty({ id: "1" }, "id"),
|
|
282
|
+
* true,
|
|
283
|
+
* )
|
|
284
|
+
*
|
|
285
|
+
* // data-last (pipeable); a null value fails the guard
|
|
286
|
+
* assert.deepStrictEqual(
|
|
287
|
+
* pipe({ id: null }, StructX.hasNotNullableProperty("id")),
|
|
288
|
+
* false,
|
|
289
|
+
* )
|
|
290
|
+
* ```
|
|
291
|
+
*
|
|
292
|
+
* @category refinements
|
|
293
|
+
* @since 0.0.0
|
|
294
|
+
*/
|
|
295
|
+
export const hasNotNullableProperty = dual<
|
|
296
|
+
<T, K extends keyof T>(
|
|
297
|
+
key: K,
|
|
298
|
+
) => (object: T) => object is T & Record<K, NonNullable<T[K]>>,
|
|
299
|
+
<T, K extends keyof T>(
|
|
300
|
+
object: T,
|
|
301
|
+
key: K,
|
|
302
|
+
) => object is T & Record<K, NonNullable<T[K]>>
|
|
303
|
+
>(
|
|
304
|
+
2,
|
|
305
|
+
<T, K extends keyof T>(
|
|
306
|
+
object: T,
|
|
307
|
+
key: K,
|
|
308
|
+
): object is T & Record<K, NonNullable<T[K]>> =>
|
|
309
|
+
Predicate.hasProperty(object, key) && Predicate.isNotNullish(object[key]),
|
|
310
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as StructX from "./StructX";
|