@1nkvi/utils 0.1.3 → 0.3.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 (51) hide show
  1. package/README.md +23 -193
  2. package/build/artifacts/src/array/createArray.d.ts +9 -0
  3. package/build/artifacts/src/array/createArray.js +11 -0
  4. package/build/artifacts/src/array/createRangeArray.d.ts +9 -0
  5. package/build/artifacts/src/array/createRangeArray.js +11 -0
  6. package/build/artifacts/src/array/index.d.ts +3 -0
  7. package/build/artifacts/src/array/index.js +3 -0
  8. package/build/artifacts/src/guard/index.d.ts +2 -0
  9. package/build/artifacts/src/guard/index.js +2 -0
  10. package/build/artifacts/src/guard/triggerExhaustiveSwitch.d.ts +14 -0
  11. package/build/artifacts/src/guard/triggerExhaustiveSwitch.js +17 -0
  12. package/build/artifacts/src/guard/utils.d.ts +4 -0
  13. package/build/artifacts/src/guard/utils.js +14 -0
  14. package/build/artifacts/src/index.d.ts +7 -5
  15. package/build/artifacts/src/index.js +7 -5
  16. package/build/artifacts/src/map/index.d.ts +1 -0
  17. package/build/artifacts/src/map/index.js +1 -0
  18. package/build/artifacts/src/map/upsert.d.ts +15 -0
  19. package/build/artifacts/src/map/upsert.js +24 -0
  20. package/build/artifacts/src/merge/DeepPartial.d.ts +3 -0
  21. package/build/artifacts/src/merge/index.d.ts +3 -0
  22. package/build/artifacts/src/merge/index.js +3 -0
  23. package/build/artifacts/src/merge/isInvalidPrimitive.js +1 -1
  24. package/build/artifacts/src/merge/merge.d.ts +1 -1
  25. package/build/artifacts/src/merge/merge.js +2 -2
  26. package/build/artifacts/src/merge/mergeArrays.js +1 -1
  27. package/build/artifacts/src/object/index.d.ts +1 -0
  28. package/build/artifacts/src/object/index.js +1 -0
  29. package/build/artifacts/src/object/replace.d.ts +7 -0
  30. package/build/artifacts/src/object/replace.js +27 -0
  31. package/build/artifacts/src/random/index.d.ts +1 -0
  32. package/build/artifacts/src/random/index.js +1 -0
  33. package/build/artifacts/src/random/random.d.ts +63 -0
  34. package/build/artifacts/src/random/random.js +57 -0
  35. package/build/artifacts/src/types/DeepPartial.d.ts +6 -0
  36. package/build/artifacts/src/types/DeepPartial.js +1 -0
  37. package/build/artifacts/src/types/DotPaths.d.ts +14 -0
  38. package/build/artifacts/src/types/DotPaths.js +1 -0
  39. package/build/artifacts/src/types/DotPathsWithArrayIndex.d.ts +21 -0
  40. package/build/artifacts/src/types/DotPathsWithArrayIndex.js +1 -0
  41. package/build/artifacts/src/types/FlatObject.d.ts +58 -0
  42. package/build/artifacts/src/types/FlatObject.js +1 -0
  43. package/build/artifacts/src/types/IsPlainObject.d.ts +15 -0
  44. package/build/artifacts/src/types/IsPlainObject.js +1 -0
  45. package/build/artifacts/src/types/LeafDotPaths.d.ts +20 -0
  46. package/build/artifacts/src/types/LeafDotPaths.js +1 -0
  47. package/build/artifacts/src/types/index.d.ts +6 -0
  48. package/build/artifacts/src/types/index.js +6 -0
  49. package/package.json +1 -1
  50. /package/build/artifacts/src/{tuplify.d.ts → array/tuplify.d.ts} +0 -0
  51. /package/build/artifacts/src/{tuplify.js → array/tuplify.js} +0 -0
package/README.md CHANGED
@@ -1,206 +1,36 @@
1
1
  # @1nkvi/utils
2
2
 
3
- Helper functions for **deeply merging** partial objects and arrays, plus a few small array/value utilities. Give it a
4
- list of partial objects and it composes them into one — overriding primitives, concatenating and de-duplicating arrays,
5
- and merging nested objects recursively — while filtering out "empty" values like `null`, `undefined` and `""`.
6
-
7
- ## Quick start
8
-
9
- ```ts
10
- import { merge } from "@1nkvi/utils"
11
-
12
- merge([{ id: 1 }, { name: "Ada" }, { name: "Ada Lovelace" }])
13
- // → { id: 1, name: "Ada Lovelace" }
14
- ```
15
-
16
- Every export is available from the package root:
17
-
18
- ```ts
19
- import {
20
- merge,
21
- mergeArrays,
22
- filterValidPrimitiveArrayValues,
23
- tuplify,
24
- isPlainObject,
25
- isEmptyObject,
26
- isEmptyArray,
27
- isInvalidNumber,
28
- } from "@1nkvi/utils"
29
- ```
30
-
31
- ---
32
-
33
- ## `merge(values, options?)`
34
-
35
- Deeply merges an array of partial objects (left to right) into a single object.
36
-
37
- ```ts
38
- merge<TData>(values: Array<Partial<TData>>, options?: MergeOptions): Partial<TData>
3
+ ```shell
4
+ pnpm i @1nkvi/utils
39
5
  ```
40
6
 
41
- The result is a _deep partial_ of `TData` it contains every key seen across the inputs that passed validation, but no
42
- field is guaranteed to be present.
7
+ A small collection of TypeScript helper functions and utility types. Its centerpiece is **deeply merging** partial
8
+ objects and arrays composing a list of partials into one while overriding primitives, concatenating and de-duplicating
9
+ arrays, and merging nested objects recursively, all while filtering out "empty" values like `null`, `undefined` and
10
+ `""`. Alongside it are a handful of array, map, randomness, and type-guard utilities, plus reusable object/path utility
11
+ types.
43
12
 
44
- ### How it merges
13
+ ## Changelog
45
14
 
46
- - **Primitives** — the last _valid_ value wins. Invalid values (see [Invalid primitives](#invalid-primitives)) are
47
- skipped, so a later `""` or `null` won't clobber an earlier real value.
48
- - **Arrays** — concatenated, then primitive values are de-duplicated. (Objects/arrays nested inside an array are kept
49
- as-is.)
50
- - **Objects** — merged recursively, to any depth.
51
- - **Mixed primitive vs. array** for the same key — the array wins by default. With `enableSingleValueArrays`, the single
52
- value is wrapped into the array instead so nothing is lost.
53
- - Inputs are **never mutated**; a new object is returned.
15
+ [Changelog link](./CHANGELOG.md)
54
16
 
55
- ### Examples
56
-
57
- ```ts
58
- // Compose keys from several partials
59
- merge([{ int: 1 }, { float: 2.3 }, { text: "Hello" }])
60
- // → { int: 1, float: 2.3, text: "Hello" }
61
-
62
- // Later valid primitives override earlier ones; "" is ignored
63
- merge([{ int: 1, text: "Hello" }, { int: 2, text: "" }, { int: 3 }])
64
- // → { int: 3, text: "Hello" }
65
-
66
- // Arrays concatenate and de-duplicate
67
- merge([{ nums: [1, 2, 3] }, { nums: [4] }])
68
- // → { nums: [1, 2, 3, 4] }
69
-
70
- // Primitive vs. array on the same key — the array wins
71
- merge([{ text: "first" }, { text: ["second", "third"] }])
72
- // → { text: ["second", "third"] }
73
-
74
- // ...unless you opt into folding the single value in
75
- merge([{ text: "first" }, { text: ["second", "third"] }], { enableSingleValueArrays: true })
76
- // → { text: ["first", "second", "third"] }
77
-
78
- // Nested objects and arrays of objects merge recursively
79
- merge([{ node: [{ text: "A", children: [{ text: "A.1" }] }] }, { node: [{ int: 20 }] }])
80
- // → { node: [{ text: "A", children: [{ text: "A.1" }] }, { int: 20 }] }
81
- ```
82
-
83
- ### `MergeOptions`
84
-
85
- | Option | Default | Effect |
86
- | -------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
87
- | `enableSingleValueArrays` | `false` | When a key mixes a single value and an array, wrap the single value into the array (instead of letting the array replace it). |
88
- | `disableDistinctPrimitiveFilter` | `false` | Keep duplicate primitives in merged arrays instead of de-duplicating. |
89
- | `nullIsValid` | `false` | Treat `null` as a valid value (don't filter it out). |
90
- | `undefinedIsValid` | `false` | Treat `undefined` as a valid value. |
91
- | `emptyStringIsValid` | `false` | Treat `""` as a valid value. |
92
- | `validators` | `[]` | Extra predicates `(value) => boolean`; returning `true` marks a value **invalid** and filters it out. |
93
-
94
- ---
95
-
96
- ## `mergeArrays(arrays, options?)`
97
-
98
- Flattens several arrays into one. By default, primitive values are de-duplicated and invalid primitives are dropped.
99
-
100
- ```ts
101
- mergeArrays<TData>(arrays: TData[][], options?: MergeArraysOptions): TData[]
102
- ```
103
-
104
- ```ts
105
- mergeArrays([["first"], ["second"], ["third", "fourth"]])
106
- // → ["first", "second", "third", "fourth"]
107
-
108
- // Duplicates removed
109
- mergeArrays([["first"], ["first", "second"]])
110
- // → ["first", "second"]
111
-
112
- // Invalid primitives ("" here) are filtered out
113
- mergeArrays([["first"], [""]])
114
- // → ["first"]
115
-
116
- // Keep everything as-is
117
- mergeArrays([["first"], ["first", ""]], { disableDistinctPrimitiveFilter: true })
118
- // → ["first", "first", ""]
119
- ```
120
-
121
- ### `MergeArraysOptions`
122
-
123
- | Option | Default | Effect |
124
- | -------------------------------- | ------- | -------------------------------------------------------------------- |
125
- | `disableDistinctPrimitiveFilter` | `false` | Keep duplicates and invalid primitives instead of filtering. |
126
- | `nullIsValid` | `false` | Treat `null` as valid. |
127
- | `undefinedIsValid` | `false` | Treat `undefined` as valid. |
128
- | `emptyStringIsValid` | `false` | Treat `""` as valid. |
129
- | `validators` | `[]` | Extra `(value) => boolean` predicates; `true` marks a value invalid. |
130
-
131
- ---
132
-
133
- ## `filterValidPrimitiveArrayValues(flatArray, options?)`
134
-
135
- Takes a single flat array, removes invalid primitives, and de-duplicates the rest.
136
-
137
- ```ts
138
- filterValidPrimitiveArrayValues<TData>(flatArray: TData[], options?): TData[]
139
- ```
140
-
141
- ```ts
142
- filterValidPrimitiveArrayValues(["first", "", null, "second", "second"])
143
- // → ["first", "second"]
144
- ```
145
-
146
- Accepts the same validity overrides as above (`nullIsValid`, `undefinedIsValid`, `emptyStringIsValid`).
147
-
148
- ---
149
-
150
- ## Invalid primitives
151
-
152
- The merge/filter functions skip values considered "empty" or "meaningless". By default these are:
153
-
154
- - `null`
155
- - `undefined`
156
- - empty string `""`
157
- - `NaN`
158
- - `Infinity` and `-Infinity`
159
- - empty array `[]`
160
- - empty object `{}`
161
-
162
- Note that **`0` and `false` are valid** — they are kept. You can relax the defaults per call with `nullIsValid` /
163
- `undefinedIsValid` / `emptyStringIsValid`, or add your own rules via `validators`:
17
+ ## Quick start
164
18
 
165
19
  ```ts
166
- // Drop any string that starts with a hash
167
- merge([{ tag: "release" }, { tag: "#draft" }], {
168
- validators: [(v) => typeof v === "string" && v.startsWith("#")],
169
- })
170
- // → { tag: "release" }
171
- ```
172
-
173
- ---
174
-
175
- ## `tuplify(value)`
176
-
177
- Wraps a single value into a one-element array, and leaves an existing array untouched. Handy for normalizing a `T | T[]`
178
- value into `T[]`.
20
+ import { merge } from "@1nkvi/utils"
179
21
 
180
- ```ts
181
- tuplify("a") // → ["a"]
182
- tuplify(["a", "b"]) // → ["a", "b"]
183
- tuplify([]) // → []
22
+ merge([{ id: 1 }, { name: "Ada" }, { name: "Ada Lovelace" }])
23
+ // → { id: 1, name: "Ada Lovelace" }
184
24
  ```
185
25
 
186
- ---
187
-
188
- ## Small value helpers
26
+ ## Modules
189
27
 
190
- Type-guards and number checks used internally, exposed for convenience:
191
-
192
- | Function | Returns `true` when… |
193
- | -------------------- | --------------------------------------------------------- |
194
- | `isPlainObject(v)` | `v` is a non-null object that isn't an array |
195
- | `isEmptyObject(v)` | `v` is a plain object with no own keys (`{}`) |
196
- | `isEmptyArray(v)` | `v` is an array of length `0` (`[]`) |
197
- | `isInvalidNumber(v)` | `v` is a number that is `NaN` or not finite (`±Infinity`) |
198
-
199
- ```ts
200
- isPlainObject({ a: 1 }) // → true
201
- isPlainObject([]) // → false
202
- isEmptyObject({}) // → true
203
- isEmptyArray([]) // → true
204
- isInvalidNumber(NaN) // → true
205
- isInvalidNumber(42) // → false
206
- ```
28
+ | Module | What it does |
29
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | [array](./docs/array.md) | Create and normalize arrays: `tuplify` a value into an array, or build index/range arrays with `createArray` and `createRangeArray`. |
31
+ | [guard](./docs/guard.md) | Type guards and runtime checks: `isPlainObject`, `isEmptyObject`, `isEmptyArray`, `isInvalidNumber`, plus `triggerExhaustiveSwitch` for exhaustiveness. |
32
+ | [map](./docs/map.md) | `upsert` a `Map` entry — insert a value, or combine it with the existing one via a callback. |
33
+ | [merge](./docs/merge.md) | Deeply `merge` partial objects, `mergeArrays` with de-duplication, and `filterValidPrimitiveArrayValues`, all skipping "empty" values. |
34
+ | [object](./docs/object.md) | `replace` a single deeply nested value by dot-notation path — immutably and type-safely, without touching its siblings. |
35
+ | [random](./docs/random.md) | Random integers: `random` within inclusive/exclusive bounds, and `randomMarginalChange` to nudge a value by fixed and percentage margins. |
36
+ | [types](./docs/types.md) | Utility types for nested objects: `DeepPartial`, `DotPaths`, `LeafDotPaths`, `DotPathsWithArrayIndex`, `FlatObject`, `IsPlainObject`. |
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Creates array from `startIndex` to `length¨
3
+ *
4
+ * Useful for quickly creating skeleton placeholders
5
+ *
6
+ * @param length size of the array
7
+ * @param startIndex defaults to 0
8
+ */
9
+ export declare function createArray(length: number, startIndex?: number): number[];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Creates array from `startIndex` to `length¨
3
+ *
4
+ * Useful for quickly creating skeleton placeholders
5
+ *
6
+ * @param length size of the array
7
+ * @param startIndex defaults to 0
8
+ */
9
+ export function createArray(length, startIndex = 0) {
10
+ return Array.from({ length }, (_, i) => i + startIndex);
11
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns array of numbers from interval `<min, max>`
3
+ * @param min min value (inclusive)
4
+ * @param max max value (inclusive)
5
+ * @example ```tsx
6
+ * createRangeArray(-2, 1) // [-2, -1, 0, 1]
7
+ * ```
8
+ */
9
+ export declare function createRangeArray(min: number, max: number): number[];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Returns array of numbers from interval `<min, max>`
3
+ * @param min min value (inclusive)
4
+ * @param max max value (inclusive)
5
+ * @example ```tsx
6
+ * createRangeArray(-2, 1) // [-2, -1, 0, 1]
7
+ * ```
8
+ */
9
+ export function createRangeArray(min, max) {
10
+ return Array.from({ length: max - min + 1 }, (_, i) => min + i);
11
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./tuplify";
2
+ export * from "./createArray";
3
+ export * from "./createRangeArray";
@@ -0,0 +1,3 @@
1
+ export * from "./tuplify";
2
+ export * from "./createArray";
3
+ export * from "./createRangeArray";
@@ -0,0 +1,2 @@
1
+ export * from "./triggerExhaustiveSwitch";
2
+ export * from "./utils";
@@ -0,0 +1,2 @@
1
+ export * from "./triggerExhaustiveSwitch";
2
+ export * from "./utils";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Used for checking that all values are accounted for in a switch
3
+ * ```typescript
4
+ * type Test = "a"|"b"|"c"
5
+ * switch(test) {
6
+ * case "a": ...
7
+ * case "b": ...
8
+ * default: triggerExhaustiveSwitch(test, "My component name")
9
+ * }
10
+ * ```
11
+ * @param value value in switch
12
+ * @param componentName name used for debugging purposes. Usually name of the component where switch lives
13
+ */
14
+ export declare function triggerExhaustiveSwitch(value: never, componentName: string): never;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Used for checking that all values are accounted for in a switch
3
+ * ```typescript
4
+ * type Test = "a"|"b"|"c"
5
+ * switch(test) {
6
+ * case "a": ...
7
+ * case "b": ...
8
+ * default: triggerExhaustiveSwitch(test, "My component name")
9
+ * }
10
+ * ```
11
+ * @param value value in switch
12
+ * @param componentName name used for debugging purposes. Usually name of the component where switch lives
13
+ */
14
+ export function triggerExhaustiveSwitch(value, componentName) {
15
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
16
+ throw new Error(`Unhandled ${componentName} type for value [${value}]`);
17
+ }
@@ -0,0 +1,4 @@
1
+ export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
2
+ export declare function isEmptyObject(v: unknown): v is object;
3
+ export declare function isEmptyArray(v: unknown): v is Array<unknown>;
4
+ export declare function isInvalidNumber(v: unknown): boolean;
@@ -0,0 +1,14 @@
1
+ export function isPlainObject(v) {
2
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3
+ }
4
+ export function isEmptyObject(v) {
5
+ if (!isPlainObject(v))
6
+ return false;
7
+ return Object.keys(v).length === 0;
8
+ }
9
+ export function isEmptyArray(v) {
10
+ return Array.isArray(v) && v.length === 0;
11
+ }
12
+ export function isInvalidNumber(v) {
13
+ return typeof v === "number" && (!Number.isFinite(v) || Number.isNaN(v));
14
+ }
@@ -1,5 +1,7 @@
1
- export * from "./merge/merge";
2
- export * from "./merge/mergeArrays";
3
- export * from "./merge/utils";
4
- export * from "./merge/filterValidPrimitiveArrayValues";
5
- export * from "./tuplify";
1
+ export * from "./array";
2
+ export * from "./merge";
3
+ export * from "./guard";
4
+ export * from "./map";
5
+ export * from "./object";
6
+ export * from "./random";
7
+ export * from "./types";
@@ -1,5 +1,7 @@
1
- export * from "./merge/merge";
2
- export * from "./merge/mergeArrays";
3
- export * from "./merge/utils";
4
- export * from "./merge/filterValidPrimitiveArrayValues";
5
- export * from "./tuplify";
1
+ export * from "./array";
2
+ export * from "./merge";
3
+ export * from "./guard";
4
+ export * from "./map";
5
+ export * from "./object";
6
+ export * from "./random";
7
+ export * from "./types";
@@ -0,0 +1 @@
1
+ export * from "./upsert";
@@ -0,0 +1 @@
1
+ export * from "./upsert";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Upserts existing map by `value`, following the `fn` behavior
3
+ * @param map existing map
4
+ * @param key key to be upserted
5
+ * @param value value to be upserted
6
+ * @param fn function used when value exists in the map
7
+ * @example ```tsx
8
+ * const map = new Map<string, string[]>()
9
+ * map.set("a", ["1", "2"])
10
+ * upsert(map, "a", ["3"], (prev, value) => [...prev, ...value])
11
+ * // output: ["1", "2", "3"]
12
+ * ```
13
+ * @remarks if key does not exist, sets the value
14
+ */
15
+ export declare function upsert<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, value: TValue, fn: (prev: TValue, value: TValue) => TValue): TValue;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Upserts existing map by `value`, following the `fn` behavior
3
+ * @param map existing map
4
+ * @param key key to be upserted
5
+ * @param value value to be upserted
6
+ * @param fn function used when value exists in the map
7
+ * @example ```tsx
8
+ * const map = new Map<string, string[]>()
9
+ * map.set("a", ["1", "2"])
10
+ * upsert(map, "a", ["3"], (prev, value) => [...prev, ...value])
11
+ * // output: ["1", "2", "3"]
12
+ * ```
13
+ * @remarks if key does not exist, sets the value
14
+ */
15
+ export function upsert(map, key, value, fn) {
16
+ if (map.has(key)) {
17
+ const prev = map.get(key);
18
+ const next = fn(prev, value);
19
+ map.set(key, next);
20
+ return next;
21
+ }
22
+ map.set(key, value);
23
+ return value;
24
+ }
@@ -1,3 +1,6 @@
1
+ /**
2
+ * Expects json object. Walks through the children and makes all keys optional
3
+ */
1
4
  export type DeepPartial<T> = T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends object ? {
2
5
  [K in keyof T]?: DeepPartial<T[K]>;
3
6
  } : T;
@@ -0,0 +1,3 @@
1
+ export * from "./merge";
2
+ export * from "./mergeArrays";
3
+ export * from "./filterValidPrimitiveArrayValues";
@@ -0,0 +1,3 @@
1
+ export * from "./merge";
2
+ export * from "./mergeArrays";
3
+ export * from "./filterValidPrimitiveArrayValues";
@@ -1,4 +1,4 @@
1
- import { isEmptyArray, isEmptyObject, isInvalidNumber } from "../merge/utils";
1
+ import { isEmptyArray, isEmptyObject, isInvalidNumber } from "../guard/utils";
2
2
  export function isInvalidPrimitive(value, options) {
3
3
  if (options?.validators) {
4
4
  const customValidatorResult = options.validators?.some((validator) => validator(value));
@@ -1,5 +1,5 @@
1
1
  import { type MergeArraysOptions } from "../merge/mergeArrays";
2
- import type { DeepPartial } from "../merge/DeepPartial";
2
+ import type { DeepPartial } from "../types/DeepPartial";
3
3
  export type MergeOptions<TData> = Omit<MergeArraysOptions<TData>, "validators"> & {
4
4
  /**
5
5
  * If `true` - merging `TValue | TValue[]` results in `TValue[]`
@@ -1,6 +1,6 @@
1
1
  import { isInvalidPrimitive } from "../merge/isInvalidPrimitive";
2
- import { isPlainObject } from "../merge/utils";
3
- import { tuplify } from "../tuplify";
2
+ import { isPlainObject } from "../guard/utils";
3
+ import { tuplify } from "../array/tuplify";
4
4
  import { mergeArrays } from "../merge/mergeArrays";
5
5
  /**
6
6
  * Deeply merges inputs into new object. Generally goes through the following steps:
@@ -1,4 +1,4 @@
1
- import { isPlainObject } from "../merge/utils";
1
+ import { isPlainObject } from "../guard/utils";
2
2
  import { filterValidPrimitiveArrayValues } from "../merge/filterValidPrimitiveArrayValues";
3
3
  export function mergeArrays(arrays, options) {
4
4
  const merged = arrays.flat();
@@ -0,0 +1 @@
1
+ export * from "./replace";
@@ -0,0 +1 @@
1
+ export * from "./replace";
@@ -0,0 +1,7 @@
1
+ import type { FlatObject } from "../../src";
2
+ /**
3
+ * Replaces a single deeply nested value, addressed by a dot‑notation path, and
4
+ * returns a new object — the input is left untouched. The `value` type is
5
+ * derived from {@link FlatObject}, so it must match the type at `key`.
6
+ */
7
+ export declare function replace<TObject, TKey extends keyof FlatObject<TObject> & string>(object: TObject, key: TKey, value: FlatObject<TObject>[TKey]): TObject;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Replaces a single deeply nested value, addressed by a dot‑notation path, and
3
+ * returns a new object — the input is left untouched. The `value` type is
4
+ * derived from {@link FlatObject}, so it must match the type at `key`.
5
+ */
6
+ export function replace(object, key, value) {
7
+ return setDeep(object, key.split("."), value);
8
+ }
9
+ /**
10
+ * Immutably sets `value` at the dot‑notation `path`, cloning every container
11
+ * along the way. Numeric segments create/clone arrays, everything else objects,
12
+ * so a single element or key can be replaced without touching its siblings.
13
+ */
14
+ function setDeep(target, path, value) {
15
+ const [head, ...rest] = path;
16
+ const clone = Array.isArray(target)
17
+ ? [...target]
18
+ : { ...target };
19
+ if (rest.length === 0) {
20
+ clone[head] = value;
21
+ return clone;
22
+ }
23
+ const child = target?.[head];
24
+ const nextSegmentIsIndex = /^\d+$/.test(rest[0]);
25
+ clone[head] = setDeep(child ?? (nextSegmentIsIndex ? [] : {}), rest, value);
26
+ return clone;
27
+ }
@@ -0,0 +1 @@
1
+ export * from "../random/random";
@@ -0,0 +1 @@
1
+ export * from "../random/random";
@@ -0,0 +1,63 @@
1
+ export interface RandomOptions {
2
+ excludeMin?: true;
3
+ excludeMax?: true;
4
+ }
5
+ /**
6
+ * Returns random integer number between `min` and `max` based on the `options`
7
+ * @param min min value (inclusive by default). Must be less than `max`
8
+ * @param max max value (inclusive by default). Must be greater than `min`
9
+ * @param options further options for modifying generator behavior
10
+ *
11
+ * @example ```tsx
12
+ * random(1, 10)
13
+ * // returns single value from inclusive interval <1, 10>
14
+ * ```
15
+ */
16
+ export declare function random(min: number, max: number, options?: RandomOptions): number;
17
+ export interface RandomMarginalChangeOptions {
18
+ /**
19
+ * Value added to the `min` in the random function
20
+ * @default 0
21
+ */
22
+ minFixed?: number;
23
+ /**
24
+ * Value added to the `max` in the random function
25
+ * @default 0
26
+ */
27
+ maxFixed?: number;
28
+ /**
29
+ * Percentage value (as decimal) multiplied the resulting `min` in the random function
30
+ * @default 1.00
31
+ */
32
+ minPercentage?: number;
33
+ /**
34
+ * Percentage value (as decimal) multiplied the resulting `max` in the random function
35
+ * @default 1.00
36
+ */
37
+ maxPercentage?: number;
38
+ /**
39
+ * Return this number if computed result is less than absolute minimum
40
+ */
41
+ absoluteMin?: number;
42
+ /**
43
+ * Return this number if computed result is greater than absolute maximum
44
+ */
45
+ absoluteMax?: number;
46
+ }
47
+ /**
48
+ * Takes initial value and creates marginal change based on options values.
49
+ *
50
+ * Insert percentages as decimals - 100% = 1.00; 1% = 0.01 calculated from initial value.
51
+ * @example
52
+ * ```typescript
53
+ * randomMarginalChange(100, {minFixed: -10, maxFixed: 10, minPercentage: 0.9, maxPercentage: 1.1})
54
+ * // results in interval min: (100 - 10) * 0.9; max: (100 + 10) * 1.1
55
+ * // where result will be <89; 121>
56
+ *
57
+ * ```
58
+ * @remark Returns floored random value
59
+ *
60
+ * @param value initial value to be derived
61
+ * @param options fixed values default to 0, percentage values defaults to 1
62
+ */
63
+ export declare function randomMarginalChange(value: number, options: RandomMarginalChangeOptions): number;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Returns random integer number between `min` and `max` based on the `options`
3
+ * @param min min value (inclusive by default). Must be less than `max`
4
+ * @param max max value (inclusive by default). Must be greater than `min`
5
+ * @param options further options for modifying generator behavior
6
+ *
7
+ * @example ```tsx
8
+ * random(1, 10)
9
+ * // returns single value from inclusive interval <1, 10>
10
+ * ```
11
+ */
12
+ export function random(min, max, options) {
13
+ if (min === max) {
14
+ return Math.floor(min);
15
+ }
16
+ if (min > max || max < min) {
17
+ throw new Error(`Min must be greater than max. Got min: ${min}, max: ${max}`);
18
+ }
19
+ if (Math.abs(Math.floor(min) - Math.floor(max)) === 1 && options?.excludeMin && options?.excludeMax) {
20
+ throw new Error(`Cannot generate random non-inclusive value from interval (${min}; ${max})`);
21
+ }
22
+ if (options?.excludeMin) {
23
+ return Math.floor(Math.random() * (max - min)) + min + 1;
24
+ }
25
+ if (options?.excludeMax) {
26
+ return Math.floor(Math.random() * (max - min)) + min;
27
+ }
28
+ return Math.floor(Math.random() * (max - min + 1)) + min;
29
+ }
30
+ /**
31
+ * Takes initial value and creates marginal change based on options values.
32
+ *
33
+ * Insert percentages as decimals - 100% = 1.00; 1% = 0.01 calculated from initial value.
34
+ * @example
35
+ * ```typescript
36
+ * randomMarginalChange(100, {minFixed: -10, maxFixed: 10, minPercentage: 0.9, maxPercentage: 1.1})
37
+ * // results in interval min: (100 - 10) * 0.9; max: (100 + 10) * 1.1
38
+ * // where result will be <89; 121>
39
+ *
40
+ * ```
41
+ * @remark Returns floored random value
42
+ *
43
+ * @param value initial value to be derived
44
+ * @param options fixed values default to 0, percentage values defaults to 1
45
+ */
46
+ export function randomMarginalChange(value, options) {
47
+ const min = (value + (options.minFixed ?? 0)) * (options.minPercentage ?? 1);
48
+ const max = (value + (options.maxFixed ?? 0)) * (options.maxPercentage ?? 1);
49
+ const computedResult = Math.round(random(min, max));
50
+ if (options.absoluteMin && computedResult < options.absoluteMin) {
51
+ return options.absoluteMin;
52
+ }
53
+ if (options.absoluteMax && computedResult > options.absoluteMax) {
54
+ return options.absoluteMax;
55
+ }
56
+ return computedResult;
57
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Expects json object. Walks through the children and makes all keys optional
3
+ */
4
+ export type DeepPartial<T> = T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends object ? {
5
+ [K in keyof T]?: DeepPartial<T[K]>;
6
+ } : T;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { IsPlainObject } from "../types/IsPlainObject";
2
+ /**
3
+ * Recursively builds a union of all nested property paths in dot‑notation.
4
+ * Only plain objects are traversed; primitives, arrays, and functions
5
+ * produce their key directly without recursion.
6
+ *
7
+ * @example ```tsx
8
+ * { a: string, b: { c: { d: number } } }
9
+ * // "a" | "b.c.d"
10
+ * ```
11
+ */
12
+ export type DotPaths<TObject> = {
13
+ [TKey in keyof TObject]: IsPlainObject<TObject[TKey]> extends true ? (TKey & string) | `${TKey & string}.${DotPaths<TObject[TKey]>}` : TKey & string;
14
+ }[keyof TObject];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { IsPlainObject } from "../types/IsPlainObject";
2
+ /**
3
+ * Recursively builds dot‑notation paths, including numeric indices for arrays.
4
+ *
5
+ * @typeParam T - The object or array type to extract paths from.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * type Example = {
10
+ * users: [
11
+ * { name: string }
12
+ * ];
13
+ * };
14
+ *
15
+ * type Paths = DotPathsWithArrayIndex<Example>;
16
+ * // "users" | "users.0" | "users.0.name"
17
+ * ```
18
+ */
19
+ export type DotPathsWithArrayIndex<TObject> = TObject extends unknown[] ? TObject extends (infer TElement)[] ? TElement extends unknown[] ? `${number}` | `${number}.${DotPathsWithArrayIndex<TElement>}` : IsPlainObject<TElement> extends true ? `${number}` | `${number}.${DotPathsWithArrayIndex<TElement>}` : `${number}` : never : {
20
+ [TKey in keyof TObject]: TObject[TKey] extends unknown[] ? (TKey & string) | `${TKey & string}.${DotPathsWithArrayIndex<TObject[TKey]>}` : IsPlainObject<TObject[TKey]> extends true ? (TKey & string) | `${TKey & string}.${DotPathsWithArrayIndex<TObject[TKey]>}` : TKey & string;
21
+ }[keyof TObject];
@@ -0,0 +1,58 @@
1
+ import type { IsPlainObject } from "../types/IsPlainObject";
2
+ /**
3
+ * Collapses a union of objects into a single intersected object so that the
4
+ * per‑key results below merge into one flat lookup map instead of a union.
5
+ */
6
+ type UnionToIntersection<TUnion> = (TUnion extends unknown ? (arg: TUnion) => void : never) extends (arg: infer TIntersection) => void ? TIntersection : never;
7
+ /**
8
+ * Produces a flattened object type where keys are dot‑notation paths and
9
+ * values are the corresponding values. Both intermediate paths (objects and
10
+ * arrays) and leaf paths are emitted, so a whole nested object/array can be
11
+ * addressed just as safely as a leaf. Arrays are descended using a generic
12
+ * numeric index (`${number}`), so element paths remain type‑safe.
13
+ *
14
+ * @typeParam T - The object type to flatten.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * type Example = {
19
+ * a: string;
20
+ * b: { c: number };
21
+ * items: { id: string }[];
22
+ * };
23
+ *
24
+ * type Flat = FlatObject<Example>;
25
+ * // {
26
+ * // "a": string;
27
+ * // "b": { c: number };
28
+ * // "b.c": number;
29
+ * // "items": { id: string }[];
30
+ * // [k: `items.${number}`]: { id: string };
31
+ * // [k: `items.${number}.id`]: string;
32
+ * // }
33
+ * ```
34
+ */
35
+ export type FlatObject<T> = T extends readonly (infer TElement)[] ? IsPlainObject<TElement> extends true ? {
36
+ [Index in `${number}`]: TElement;
37
+ } & {
38
+ [SubKey in keyof FlatObject<TElement> & string as `${number}.${SubKey}`]: FlatObject<TElement>[SubKey];
39
+ } : TElement extends readonly unknown[] ? {
40
+ [Index in `${number}`]: TElement;
41
+ } & {
42
+ [SubKey in keyof FlatObject<TElement> & string as `${number}.${SubKey}`]: FlatObject<TElement>[SubKey];
43
+ } : {
44
+ [Index in `${number}`]: TElement;
45
+ } : UnionToIntersection<{
46
+ [Key in keyof T & string]: IsPlainObject<T[Key]> extends true ? {
47
+ [FlatKey in Key]: T[Key];
48
+ } & {
49
+ [SubKey in keyof FlatObject<T[Key]> & string as `${Key}.${SubKey}`]: FlatObject<T[Key]>[SubKey];
50
+ } : T[Key] extends readonly unknown[] ? {
51
+ [FlatKey in Key]: T[Key];
52
+ } & {
53
+ [SubKey in keyof FlatObject<T[Key]> & string as `${Key}.${SubKey}`]: FlatObject<T[Key]>[SubKey];
54
+ } : {
55
+ [FlatKey in Key]: T[Key];
56
+ };
57
+ }[keyof T & string]>;
58
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Determines whether a type is a plain object suitable for recursive traversal.
3
+ *
4
+ * A "plain object" in this context means:
5
+ * - It is an object type
6
+ * - It is **not** an array
7
+ * - It is **not** a function
8
+ *
9
+ * This prevents recursion from descending into prototypes of arrays, functions,
10
+ * primitives, or built‑in objects, ensuring that only object literals are walked.
11
+ *
12
+ * @typeParam T - The type being checked.
13
+ * @returns `true` if `TObject` is a non‑array, non‑function object; otherwise `false`.
14
+ */
15
+ export type IsPlainObject<TObject> = TObject extends object ? TObject extends unknown[] ? false : TObject extends (...args: unknown[]) => unknown ? false : true : false;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { IsPlainObject } from "../types/IsPlainObject";
2
+ /**
3
+ * Extracts only leaf property paths (deepest keys) in dot‑notation.
4
+ *
5
+ * @typeParam T - The object type whose leaf paths should be extracted.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * type Example = {
10
+ * a: string;
11
+ * b: { c: { d: number } };
12
+ * };
13
+ *
14
+ * type Paths = LeafDotPaths<Example>;
15
+ * // "a" | "b.c.d"
16
+ * ```
17
+ */
18
+ export type LeafDotPaths<TObject> = {
19
+ [TKey in keyof TObject]: IsPlainObject<TObject[TKey]> extends true ? `${TKey & string}.${LeafDotPaths<TObject[TKey]>}` : TKey & string;
20
+ }[keyof TObject];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export * from "./DeepPartial";
2
+ export * from "./DotPaths";
3
+ export * from "./DotPathsWithArrayIndex";
4
+ export * from "./FlatObject";
5
+ export * from "./IsPlainObject";
6
+ export * from "./LeafDotPaths";
@@ -0,0 +1,6 @@
1
+ export * from "./DeepPartial";
2
+ export * from "./DotPaths";
3
+ export * from "./DotPathsWithArrayIndex";
4
+ export * from "./FlatObject";
5
+ export * from "./IsPlainObject";
6
+ export * from "./LeafDotPaths";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1nkvi/utils",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "Helper functions",
5
5
  "keywords": [
6
6
  "merge"