ts-data-forge 14.2.1 → 14.4.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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/ts-data-forge.svg)](https://www.npmjs.com/package/ts-data-forge)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/ts-data-forge.svg)](https://www.npmjs.com/package/ts-data-forge)
5
5
  [![License](https://img.shields.io/npm/l/ts-data-forge.svg)](https://github.com/noshiro-pf/mono/blob/main/libs/ts-data-forge/LICENSE)
6
- [![codecov](https://codecov.io/gh/noshiro-pf/mono/graph/badge.svg)](https://codecov.io/gh/noshiro-pf/mono)
6
+ [![codecov](https://codecov.io/gh/noshiro-pf/mono/graph/badge.svg?component=ts-data-forge)](https://codecov.io/gh/noshiro-pf/mono)
7
7
 
8
8
  **ts-data-forge** is a TypeScript utility library that provides type-safe functional programming utilities with zero runtime dependencies. It aims to enhance development robustness, maintainability, and correctness by leveraging TypeScript's powerful type system.
9
9
 
@@ -48,7 +48,6 @@ export type Stack<T> = Readonly<{
48
48
  *
49
49
  * stack.push('first');
50
50
  *
51
- * // eslint-disable-next-line unicorn/prefer-single-call
52
51
  * stack.push('second');
53
52
  *
54
53
  * assert.isFalse(stack.isEmpty);
@@ -83,7 +82,6 @@ export type Stack<T> = Readonly<{
83
82
  *
84
83
  * stack.push('first');
85
84
  *
86
- * // eslint-disable-next-line unicorn/prefer-single-call
87
85
  * stack.push('second');
88
86
  *
89
87
  * assert.isFalse(stack.isEmpty);
@@ -118,7 +116,6 @@ export type Stack<T> = Readonly<{
118
116
  *
119
117
  * stack.push('first');
120
118
  *
121
- * // eslint-disable-next-line unicorn/prefer-single-call
122
119
  * stack.push('second');
123
120
  *
124
121
  * assert.isFalse(stack.isEmpty);
@@ -156,7 +153,6 @@ export type Stack<T> = Readonly<{
156
153
  *
157
154
  * stack.push('first');
158
155
  *
159
- * // eslint-disable-next-line unicorn/prefer-single-call
160
156
  * stack.push('second');
161
157
  *
162
158
  * assert.isFalse(stack.isEmpty);
@@ -215,7 +211,6 @@ export type Stack<T> = Readonly<{
215
211
  *
216
212
  * stack.push('first');
217
213
  *
218
- * // eslint-disable-next-line unicorn/prefer-single-call
219
214
  * stack.push('second');
220
215
  *
221
216
  * assert.isFalse(stack.isEmpty);
@@ -1 +1 @@
1
- {"version":3,"file":"stack.d.mts","sourceRoot":"","sources":["../../src/collections/stack.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,QAAQ,CAAC;IAC9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACH,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC;IAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoCG;IACH,GAAG,EAAE,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CAC1B,CAAC,CAAC;AA+IH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,kBAAmB,SAAS,CAAC,EAAE,KAAG,KAAK,CAAC,CAAC,CACpC,CAAC"}
1
+ {"version":3,"file":"stack.d.mts","sourceRoot":"","sources":["../../src/collections/stack.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,QAAQ,CAAC;IAC9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC;IAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACH,GAAG,EAAE,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CAC1B,CAAC,CAAC;AA+IH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,kBAAmB,SAAS,CAAC,EAAE,KAAG,KAAK,CAAC,CAAC,CACpC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"stack.mjs","sources":["../../src/collections/stack.mts"],"sourcesContent":["import { Optional } from '../functional/index.mjs';\nimport { range } from '../iterator/index.mjs';\nimport { asUint32, Uint32 } from '../number/index.mjs';\nimport { type SizeType } from '../types.mjs';\n\n/**\n * Interface for a high-performance stack with LIFO (Last-In, First-Out)\n * behavior.\n *\n * This interface defines a mutable stack data structure where elements are\n * added to and removed from the top, following the Last-In, First-Out\n * principle. The implementation uses a dynamic array for optimal performance,\n * providing O(1) operations for both push and pop operations.\n *\n * **LIFO Behavior:**\n *\n * - **push**: Adds elements to the top of the stack\n * - **pop**: Removes and returns elements from the top of the stack\n * - The last element added is the first element to be removed\n *\n * **Performance Characteristics:**\n *\n * - Push: O(1) amortized (O(n) when buffer needs resizing)\n * - Pop: O(1) always\n * - Size/isEmpty: O(1) always\n * - Memory efficient with automatic garbage collection of removed elements\n *\n * **Use Cases:**\n *\n * - Function call management and recursion\n * - Undo/redo functionality\n * - Expression evaluation and parsing\n * - Depth-first search algorithms\n * - Backtracking algorithms\n * - Browser history management\n *\n * @template T The type of elements stored in the stack.\n */\nexport type Stack<T> = Readonly<{\n /**\n * Checks if the stack is empty.\n *\n * @example\n *\n * ```ts\n * const stack = createStack<string>();\n *\n * assert.isTrue(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 0);\n *\n * stack.push('first');\n *\n * // eslint-disable-next-line unicorn/prefer-single-call\n * stack.push('second');\n *\n * assert.isFalse(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 2);\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('second'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('first'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.none);\n *\n * const seededStack = createStack([10, 20, 30]);\n *\n * assert.isTrue(seededStack.size === 3);\n *\n * assert.deepStrictEqual(seededStack.pop(), Optional.some(30));\n * ```\n */\n isEmpty: boolean;\n\n /**\n * The number of elements in the stack.\n *\n * @example\n *\n * ```ts\n * const stack = createStack<string>();\n *\n * assert.isTrue(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 0);\n *\n * stack.push('first');\n *\n * // eslint-disable-next-line unicorn/prefer-single-call\n * stack.push('second');\n *\n * assert.isFalse(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 2);\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('second'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('first'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.none);\n *\n * const seededStack = createStack([10, 20, 30]);\n *\n * assert.isTrue(seededStack.size === 3);\n *\n * assert.deepStrictEqual(seededStack.pop(), Optional.some(30));\n * ```\n */\n size: SizeType.Arr;\n\n /**\n * Removes and returns the element at the top of the stack.\n *\n * @example\n *\n * ```ts\n * const stack = createStack<string>();\n *\n * assert.isTrue(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 0);\n *\n * stack.push('first');\n *\n * // eslint-disable-next-line unicorn/prefer-single-call\n * stack.push('second');\n *\n * assert.isFalse(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 2);\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('second'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('first'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.none);\n *\n * const seededStack = createStack([10, 20, 30]);\n *\n * assert.isTrue(seededStack.size === 3);\n *\n * assert.deepStrictEqual(seededStack.pop(), Optional.some(30));\n * ```\n *\n * @returns The element at the top of the stack, or `Optional.none` if the\n * stack is empty.\n */\n pop: () => Optional<T>;\n\n /**\n * Adds an element to the top of the stack.\n *\n * @example\n *\n * ```ts\n * const stack = createStack<string>();\n *\n * assert.isTrue(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 0);\n *\n * stack.push('first');\n *\n * // eslint-disable-next-line unicorn/prefer-single-call\n * stack.push('second');\n *\n * assert.isFalse(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 2);\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('second'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('first'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.none);\n *\n * const seededStack = createStack([10, 20, 30]);\n *\n * assert.isTrue(seededStack.size === 3);\n *\n * assert.deepStrictEqual(seededStack.pop(), Optional.some(30));\n * ```\n *\n * @param value The element to add.\n */\n push: (value: T) => void;\n}>;\n\n/**\n * Class implementation for a stack with LIFO (Last-In, First-Out) behavior\n * using a dynamic array. This implementation provides O(1) amortized push and\n * O(1) pop operations by using a resizable buffer that grows as needed.\n *\n * The underlying array automatically resizes when it becomes full, ensuring\n * that the stack can grow to accommodate any number of elements while\n * maintaining efficient operations.\n *\n * @template T The type of elements in the stack.\n * @implements Stack\n */\nclass StackClass<T> implements Stack<T> {\n /** @internal Initial capacity for new stacks. */\n static readonly #INITIAL_CAPACITY = 8;\n\n /** @internal Dynamic array to store stack elements. */\n #mut_buffer: (T | undefined)[];\n\n /** @internal Current number of elements in the stack. */\n #mut_size: Uint32;\n\n /** @internal Current capacity of the buffer. */\n #mut_capacity: Uint32;\n\n /**\n * Constructs a new StackClass instance.\n *\n * @param initialValues Optional initial values to populate the stack.\n */\n constructor(initialValues: readonly T[] = []) {\n const initialCapacity = asUint32(\n Math.max(StackClass.#INITIAL_CAPACITY, initialValues.length * 2),\n );\n\n this.#mut_buffer = Array.from<unknown, T | undefined>(\n { length: initialCapacity },\n () => undefined,\n );\n\n this.#mut_size = asUint32(0);\n\n this.#mut_capacity = initialCapacity;\n\n // Add initial values\n for (const value of initialValues) {\n this.push(value);\n }\n }\n\n /**\n * @internal\n * Resizes the buffer when it becomes full.\n * Doubles the capacity while preserving all elements.\n */\n #resize(): void {\n const newCapacity = asUint32(this.#mut_capacity * 2);\n\n const newBuffer = Array.from<unknown, T | undefined>(\n { length: newCapacity },\n () => undefined,\n );\n\n // Copy existing elements\n for (const i of range(this.#mut_size)) {\n newBuffer[i] = this.#mut_buffer[i];\n }\n\n this.#mut_buffer = newBuffer;\n\n this.#mut_capacity = newCapacity;\n }\n\n /** @inheritdoc */\n get isEmpty(): boolean {\n return this.#mut_size === 0;\n }\n\n /** @inheritdoc */\n get size(): SizeType.Arr {\n return asUint32(this.#mut_size);\n }\n\n /**\n * Removes and returns the element at the top of the stack (LIFO).\n *\n * This operation removes the element that was added most recently (last-in)\n * and returns it. If the stack is empty, returns `Optional.none`. The\n * operation is guaranteed to be O(1) and does not require any array resizing\n * or memory reallocation.\n *\n * **Time Complexity:** O(1) - constant time operation **Space Complexity:**\n * O(1) - no additional memory allocation\n *\n * @returns An Optional containing the removed element, or `Optional.none` if\n * the stack is empty.\n */\n pop(): Optional<T> {\n if (this.isEmpty) {\n return Optional.none;\n }\n\n this.#mut_size = Uint32.sub(this.#mut_size, 1);\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const element = this.#mut_buffer[this.#mut_size]!;\n\n this.#mut_buffer[this.#mut_size] = undefined; // Clear reference for garbage collection\n\n return Optional.some(element);\n }\n\n /**\n * Adds an element to the top of the stack (LIFO).\n *\n * This operation adds the element to the top of the stack, where it will be\n * the first to be popped (last-in, first-out ordering). The operation is\n * amortized O(1), meaning it's O(1) for most operations with occasional O(n)\n * when the buffer needs resizing.\n *\n * **Time Complexity:** O(1) amortized - O(n) only when buffer resize is\n * needed **Space Complexity:** O(1) - constant additional memory per element\n *\n * **Buffer Resizing:** When the internal buffer becomes full, it\n * automatically doubles in size and copies existing elements to maintain the\n * stack structure.\n *\n * @param value The element to add to the top of the stack.\n */\n push(value: T): void {\n // Resize if buffer is full\n if (this.#mut_size === this.#mut_capacity) {\n this.#resize();\n }\n\n this.#mut_buffer[this.#mut_size] = value;\n\n this.#mut_size = Uint32.add(this.#mut_size, 1);\n }\n}\n\n/**\n * Creates a new Stack instance with LIFO (Last-In, First-Out) behavior using a\n * high-performance dynamic array.\n *\n * This factory function creates an optimized stack implementation that\n * maintains excellent performance characteristics for both push and pop\n * operations. The underlying dynamic array automatically resizes to accommodate\n * growing workloads while providing predictable O(1) operations.\n *\n * **Implementation Features:**\n *\n * - **O(1) push operations** (amortized - occasionally O(n) when resizing)\n * - **O(1) pop operations** (always)\n * - **Automatic buffer resizing** - starts at 8 elements, doubles when full\n * - **Memory efficient** - garbage collects removed elements immediately\n * - **Dynamic array design** - eliminates need for complex memory management\n *\n * **Performance Benefits:**\n *\n * - No array shifting required for stack operations\n * - Minimal memory allocation overhead\n * - Predictable performance under high load\n * - Efficient memory usage with automatic cleanup\n *\n * @example\n *\n * ```ts\n * const stack = createStack<string>();\n *\n * assert.isTrue(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 0);\n *\n * stack.push('first');\n *\n * // eslint-disable-next-line unicorn/prefer-single-call\n * stack.push('second');\n *\n * assert.isFalse(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 2);\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('second'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('first'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.none);\n *\n * const seededStack = createStack([10, 20, 30]);\n *\n * assert.isTrue(seededStack.size === 3);\n *\n * assert.deepStrictEqual(seededStack.pop(), Optional.some(30));\n * ```\n *\n * @template T The type of elements stored in the stack.\n * @param initialValues Optional array of initial elements to populate the\n * stack. Elements will be popped in reverse order of how they appear in the\n * array (last array element will be popped first). If provided, the initial\n * buffer capacity will be at least twice the array length.\n * @returns A new Stack instance optimized for high-performance LIFO operations.\n */\nexport const createStack = <T,>(initialValues?: readonly T[]): Stack<T> =>\n new StackClass<T>(initialValues);\n"],"names":["Optional.none","Optional.some"],"mappings":"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"}
1
+ {"version":3,"file":"stack.mjs","sources":["../../src/collections/stack.mts"],"sourcesContent":["import { Optional } from '../functional/index.mjs';\nimport { range } from '../iterator/index.mjs';\nimport { asUint32, Uint32 } from '../number/index.mjs';\nimport { type SizeType } from '../types.mjs';\n\n/**\n * Interface for a high-performance stack with LIFO (Last-In, First-Out)\n * behavior.\n *\n * This interface defines a mutable stack data structure where elements are\n * added to and removed from the top, following the Last-In, First-Out\n * principle. The implementation uses a dynamic array for optimal performance,\n * providing O(1) operations for both push and pop operations.\n *\n * **LIFO Behavior:**\n *\n * - **push**: Adds elements to the top of the stack\n * - **pop**: Removes and returns elements from the top of the stack\n * - The last element added is the first element to be removed\n *\n * **Performance Characteristics:**\n *\n * - Push: O(1) amortized (O(n) when buffer needs resizing)\n * - Pop: O(1) always\n * - Size/isEmpty: O(1) always\n * - Memory efficient with automatic garbage collection of removed elements\n *\n * **Use Cases:**\n *\n * - Function call management and recursion\n * - Undo/redo functionality\n * - Expression evaluation and parsing\n * - Depth-first search algorithms\n * - Backtracking algorithms\n * - Browser history management\n *\n * @template T The type of elements stored in the stack.\n */\nexport type Stack<T> = Readonly<{\n /**\n * Checks if the stack is empty.\n *\n * @example\n *\n * ```ts\n * const stack = createStack<string>();\n *\n * assert.isTrue(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 0);\n *\n * stack.push('first');\n *\n * stack.push('second');\n *\n * assert.isFalse(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 2);\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('second'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('first'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.none);\n *\n * const seededStack = createStack([10, 20, 30]);\n *\n * assert.isTrue(seededStack.size === 3);\n *\n * assert.deepStrictEqual(seededStack.pop(), Optional.some(30));\n * ```\n */\n isEmpty: boolean;\n\n /**\n * The number of elements in the stack.\n *\n * @example\n *\n * ```ts\n * const stack = createStack<string>();\n *\n * assert.isTrue(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 0);\n *\n * stack.push('first');\n *\n * stack.push('second');\n *\n * assert.isFalse(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 2);\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('second'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('first'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.none);\n *\n * const seededStack = createStack([10, 20, 30]);\n *\n * assert.isTrue(seededStack.size === 3);\n *\n * assert.deepStrictEqual(seededStack.pop(), Optional.some(30));\n * ```\n */\n size: SizeType.Arr;\n\n /**\n * Removes and returns the element at the top of the stack.\n *\n * @example\n *\n * ```ts\n * const stack = createStack<string>();\n *\n * assert.isTrue(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 0);\n *\n * stack.push('first');\n *\n * stack.push('second');\n *\n * assert.isFalse(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 2);\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('second'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('first'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.none);\n *\n * const seededStack = createStack([10, 20, 30]);\n *\n * assert.isTrue(seededStack.size === 3);\n *\n * assert.deepStrictEqual(seededStack.pop(), Optional.some(30));\n * ```\n *\n * @returns The element at the top of the stack, or `Optional.none` if the\n * stack is empty.\n */\n pop: () => Optional<T>;\n\n /**\n * Adds an element to the top of the stack.\n *\n * @example\n *\n * ```ts\n * const stack = createStack<string>();\n *\n * assert.isTrue(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 0);\n *\n * stack.push('first');\n *\n * stack.push('second');\n *\n * assert.isFalse(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 2);\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('second'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('first'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.none);\n *\n * const seededStack = createStack([10, 20, 30]);\n *\n * assert.isTrue(seededStack.size === 3);\n *\n * assert.deepStrictEqual(seededStack.pop(), Optional.some(30));\n * ```\n *\n * @param value The element to add.\n */\n push: (value: T) => void;\n}>;\n\n/**\n * Class implementation for a stack with LIFO (Last-In, First-Out) behavior\n * using a dynamic array. This implementation provides O(1) amortized push and\n * O(1) pop operations by using a resizable buffer that grows as needed.\n *\n * The underlying array automatically resizes when it becomes full, ensuring\n * that the stack can grow to accommodate any number of elements while\n * maintaining efficient operations.\n *\n * @template T The type of elements in the stack.\n * @implements Stack\n */\nclass StackClass<T> implements Stack<T> {\n /** @internal Initial capacity for new stacks. */\n static readonly #INITIAL_CAPACITY = 8;\n\n /** @internal Dynamic array to store stack elements. */\n #mut_buffer: (T | undefined)[];\n\n /** @internal Current number of elements in the stack. */\n #mut_size: Uint32;\n\n /** @internal Current capacity of the buffer. */\n #mut_capacity: Uint32;\n\n /**\n * Constructs a new StackClass instance.\n *\n * @param initialValues Optional initial values to populate the stack.\n */\n constructor(initialValues: readonly T[] = []) {\n const initialCapacity = asUint32(\n Math.max(StackClass.#INITIAL_CAPACITY, initialValues.length * 2),\n );\n\n this.#mut_buffer = Array.from<unknown, T | undefined>(\n { length: initialCapacity },\n () => undefined,\n );\n\n this.#mut_size = asUint32(0);\n\n this.#mut_capacity = initialCapacity;\n\n // Add initial values\n for (const value of initialValues) {\n this.push(value);\n }\n }\n\n /**\n * @internal\n * Resizes the buffer when it becomes full.\n * Doubles the capacity while preserving all elements.\n */\n #resize(): void {\n const newCapacity = asUint32(this.#mut_capacity * 2);\n\n const newBuffer = Array.from<unknown, T | undefined>(\n { length: newCapacity },\n () => undefined,\n );\n\n // Copy existing elements\n for (const i of range(this.#mut_size)) {\n newBuffer[i] = this.#mut_buffer[i];\n }\n\n this.#mut_buffer = newBuffer;\n\n this.#mut_capacity = newCapacity;\n }\n\n /** @inheritdoc */\n get isEmpty(): boolean {\n return this.#mut_size === 0;\n }\n\n /** @inheritdoc */\n get size(): SizeType.Arr {\n return asUint32(this.#mut_size);\n }\n\n /**\n * Removes and returns the element at the top of the stack (LIFO).\n *\n * This operation removes the element that was added most recently (last-in)\n * and returns it. If the stack is empty, returns `Optional.none`. The\n * operation is guaranteed to be O(1) and does not require any array resizing\n * or memory reallocation.\n *\n * **Time Complexity:** O(1) - constant time operation **Space Complexity:**\n * O(1) - no additional memory allocation\n *\n * @returns An Optional containing the removed element, or `Optional.none` if\n * the stack is empty.\n */\n pop(): Optional<T> {\n if (this.isEmpty) {\n return Optional.none;\n }\n\n this.#mut_size = Uint32.sub(this.#mut_size, 1);\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const element = this.#mut_buffer[this.#mut_size]!;\n\n this.#mut_buffer[this.#mut_size] = undefined; // Clear reference for garbage collection\n\n return Optional.some(element);\n }\n\n /**\n * Adds an element to the top of the stack (LIFO).\n *\n * This operation adds the element to the top of the stack, where it will be\n * the first to be popped (last-in, first-out ordering). The operation is\n * amortized O(1), meaning it's O(1) for most operations with occasional O(n)\n * when the buffer needs resizing.\n *\n * **Time Complexity:** O(1) amortized - O(n) only when buffer resize is\n * needed **Space Complexity:** O(1) - constant additional memory per element\n *\n * **Buffer Resizing:** When the internal buffer becomes full, it\n * automatically doubles in size and copies existing elements to maintain the\n * stack structure.\n *\n * @param value The element to add to the top of the stack.\n */\n push(value: T): void {\n // Resize if buffer is full\n if (this.#mut_size === this.#mut_capacity) {\n this.#resize();\n }\n\n this.#mut_buffer[this.#mut_size] = value;\n\n this.#mut_size = Uint32.add(this.#mut_size, 1);\n }\n}\n\n/**\n * Creates a new Stack instance with LIFO (Last-In, First-Out) behavior using a\n * high-performance dynamic array.\n *\n * This factory function creates an optimized stack implementation that\n * maintains excellent performance characteristics for both push and pop\n * operations. The underlying dynamic array automatically resizes to accommodate\n * growing workloads while providing predictable O(1) operations.\n *\n * **Implementation Features:**\n *\n * - **O(1) push operations** (amortized - occasionally O(n) when resizing)\n * - **O(1) pop operations** (always)\n * - **Automatic buffer resizing** - starts at 8 elements, doubles when full\n * - **Memory efficient** - garbage collects removed elements immediately\n * - **Dynamic array design** - eliminates need for complex memory management\n *\n * **Performance Benefits:**\n *\n * - No array shifting required for stack operations\n * - Minimal memory allocation overhead\n * - Predictable performance under high load\n * - Efficient memory usage with automatic cleanup\n *\n * @example\n *\n * ```ts\n * const stack = createStack<string>();\n *\n * assert.isTrue(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 0);\n *\n * stack.push('first');\n *\n * stack.push('second');\n *\n * assert.isFalse(stack.isEmpty);\n *\n * assert.isTrue(stack.size === 2);\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('second'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.some('first'));\n *\n * assert.deepStrictEqual(stack.pop(), Optional.none);\n *\n * const seededStack = createStack([10, 20, 30]);\n *\n * assert.isTrue(seededStack.size === 3);\n *\n * assert.deepStrictEqual(seededStack.pop(), Optional.some(30));\n * ```\n *\n * @template T The type of elements stored in the stack.\n * @param initialValues Optional array of initial elements to populate the\n * stack. Elements will be popped in reverse order of how they appear in the\n * array (last array element will be popped first). If provided, the initial\n * buffer capacity will be at least twice the array length.\n * @returns A new Stack instance optimized for high-performance LIFO operations.\n */\nexport const createStack = <T,>(initialValues?: readonly T[]): Stack<T> =>\n new StackClass<T>(initialValues);\n"],"names":["Optional.none","Optional.some"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import { type ArrayElement, type Decrement, type DeepOmit, type DeepPick, type IsFixedLengthList, type IsUnion, type List, type ReadonlyRecord, type RelaxedExclude, type RelaxedOmit, type RelaxedPick, type StrictOmit, type StrictPick, type TypeEq, type UnknownRecord } from 'ts-type-forge';
1
+ import { type ArrayElement, type Decrement, type DeepOmit, type DeepPick, type IsFixedLengthList, type IsUnion, type List, type ReadonlyRecord, type RelaxedExclude, type RelaxedOmit, type RelaxedPick, type StrictOmit, type StrictPick, type ToString, type TypeEq, type UnknownRecord } from 'ts-type-forge';
2
+ import { Optional } from '../functional/index.mjs';
2
3
  /**
3
4
  * A collection of type-safe object utility functions providing functional
4
5
  * programming patterns for object manipulation, including pick, omit, shallow
@@ -189,6 +190,147 @@ export declare namespace Obj {
189
190
  * @returns An object created from the entries with precise typing
190
191
  */
191
192
  const fromEntries: <const Entries extends readonly (readonly [PropertyKey, unknown])[]>(entries: Entries) => IsFixedLengthList<Entries> extends true ? TsDataForgeInternals.EntriesToObject<Entries> : TsDataForgeInternals.PartialIfKeyIsUnion<TsDataForgeInternals.KeysOfEntries<Entries>, ReadonlyRecord<TsDataForgeInternals.KeysOfEntries<Entries>, TsDataForgeInternals.ValuesOfEntries<Entries>>>;
193
+ /**
194
+ * Transforms every value of a record, leaving the key set untouched.
195
+ *
196
+ * **Type Behavior**: the result is a mapped type over `keyof R`, so the keys
197
+ * — and their optional modifiers — survive verbatim:
198
+ * `{ a: number; b?: number }` maps to `{ a: V2; b?: V2 }`, and a record with
199
+ * an index signature stays total rather than becoming `Partial`.
200
+ *
201
+ * This is what `Object.fromEntries(Object.entries(record).map(...))` cannot
202
+ * express: an entries array carries no evidence that it still covers every
203
+ * key of the record it came from, so its element type is all the return type
204
+ * can be built from. Going through a mapped type instead states the
205
+ * invariant that a value-only transform preserves the key set.
206
+ *
207
+ * **Key handling**: keys reach the callback the way `Object.entries` yields
208
+ * them, so a numeric key `1` arrives as `'1'`. Symbol-keyed properties are
209
+ * not enumerated by `Object.entries`; they are dropped from both the result
210
+ * and its type.
211
+ *
212
+ * @example
213
+ *
214
+ * <!-- doc:embed:jsdoc:example:./samples/src/object/map-example.mts -->
215
+ *
216
+ * ```ts
217
+ * const scores = { alice: 1, bob: 2 } as const;
218
+ *
219
+ * const doubled = Obj.map(scores, (value) => value * 2);
220
+ *
221
+ * assert.deepStrictEqual(doubled, { alice: 2, bob: 4 });
222
+ *
223
+ * // The key is available as the second argument.
224
+ * const labelled = Obj.map(scores, (value, key) => `${key}=${value}`);
225
+ *
226
+ * assert.deepStrictEqual(labelled, { alice: 'alice=1', bob: 'bob=2' });
227
+ * ```
228
+ *
229
+ * <!-- /doc:embed:jsdoc:example:./samples/src/object/map-example.mts -->
230
+ *
231
+ * @template R - The type of the source record
232
+ * @template V2 - The type the callback returns for every key
233
+ * @param record - The record whose values are transformed
234
+ * @param mapFn - Called once per own enumerable string key, with the value
235
+ * and the key
236
+ * @returns A new record with the same keys as `record` and the mapped values
237
+ */
238
+ const map: <const R extends UnknownRecord, V2>(record: R, mapFn: (value: R[keyof R], key: TsDataForgeInternals.MappedRecordKey<R>) => V2) => TsDataForgeInternals.MappedRecord<R, V2>;
239
+ /**
240
+ * Keeps the entries whose value the predicate accepts, leaving the values
241
+ * themselves untouched.
242
+ *
243
+ * **Type Behavior**: the surviving keys are the keys of the source record,
244
+ * made optional — the predicate may reject any of them. A record keyed by an
245
+ * index signature stays total instead, because it names no specific key that
246
+ * could go missing.
247
+ *
248
+ * Passing a type guard narrows the value type of the result, which is what
249
+ * `Object.fromEntries(Object.entries(record).filter(guard))` loses.
250
+ *
251
+ * **Key handling**: keys reach the callback the way `Object.entries` yields
252
+ * them, so a numeric key `1` arrives as `'1'`. Symbol-keyed properties are
253
+ * not enumerated by `Object.entries`; they are dropped from both the result
254
+ * and its type.
255
+ *
256
+ * @example
257
+ *
258
+ * <!-- doc:embed:jsdoc:example:./samples/src/object/filter-example.mts -->
259
+ *
260
+ * ```ts
261
+ * const scores = { alice: 1, bob: 2 } as const;
262
+ *
263
+ * const passing = Obj.filter(scores, (value) => value > 1);
264
+ *
265
+ * assert.deepStrictEqual(passing, { bob: 2 });
266
+ *
267
+ * // The key is available as the second argument.
268
+ * const notAlice = Obj.filter(scores, (_value, key) => key !== 'alice');
269
+ *
270
+ * assert.deepStrictEqual(notAlice, { bob: 2 });
271
+ *
272
+ * // A type guard narrows the value type of the result.
273
+ * const mixed: UnknownRecord = { a: 1, b: 'x' } as const;
274
+ *
275
+ * const strings = Obj.filter(mixed, isString);
276
+ *
277
+ * assert.deepStrictEqual(strings, { b: 'x' });
278
+ * ```
279
+ *
280
+ * <!-- /doc:embed:jsdoc:example:./samples/src/object/filter-example.mts -->
281
+ *
282
+ * @template R - The type of the source record
283
+ * @template V2 - The type the guard narrows values to
284
+ * @param record - The record whose entries are filtered
285
+ * @param predicate - Called once per own enumerable string key, with the
286
+ * value and the key
287
+ * @returns A new record holding only the accepted entries
288
+ */
289
+ function filter<const R extends UnknownRecord, V2 extends R[keyof R]>(record: R, predicate: (value: R[keyof R], key: TsDataForgeInternals.MappedRecordKey<R>) => value is V2): TsDataForgeInternals.FilteredRecord<TsDataForgeInternals.MappedRecord<R, V2>>;
290
+ function filter<const R extends UnknownRecord>(record: R, predicate: (value: R[keyof R], key: TsDataForgeInternals.MappedRecordKey<R>) => boolean): TsDataForgeInternals.FilteredRecord<R>;
291
+ /**
292
+ * Transforms and drops entries in one pass: the callback returns
293
+ * `Optional.some(value)` to keep an entry with a new value, or
294
+ * `Optional.none` to drop it.
295
+ *
296
+ * **Type Behavior**: as with {@link filter}, the surviving keys are the keys
297
+ * of the source record made optional, and a record keyed by an index
298
+ * signature stays total. Unlike {@link filter}, every kept value takes the
299
+ * type the callback returns.
300
+ *
301
+ * `Optional` rather than `undefined` marks the dropped entries, so a record
302
+ * whose values are legitimately `undefined` stays expressible.
303
+ *
304
+ * **Key handling**: keys reach the callback the way `Object.entries` yields
305
+ * them, so a numeric key `1` arrives as `'1'`. Symbol-keyed properties are
306
+ * not enumerated by `Object.entries`; they are dropped from both the result
307
+ * and its type.
308
+ *
309
+ * @example
310
+ *
311
+ * <!-- doc:embed:jsdoc:example:./samples/src/object/filter-map-example.mts -->
312
+ *
313
+ * ```ts
314
+ * const scores = { alice: 1, bob: 2 } as const;
315
+ *
316
+ * // `Optional.some` keeps an entry with a new value, `Optional.none` drops it.
317
+ * const labelled = Obj.filterMap(scores, (value, key) =>
318
+ * value > 1 ? Optional.some(`${key}=${value}`) : Optional.none,
319
+ * );
320
+ *
321
+ * assert.deepStrictEqual(labelled, { bob: 'bob=2' });
322
+ * ```
323
+ *
324
+ * <!-- /doc:embed:jsdoc:example:./samples/src/object/filter-map-example.mts -->
325
+ *
326
+ * @template R - The type of the source record
327
+ * @template V2 - The type the callback returns for the entries it keeps
328
+ * @param record - The record whose entries are transformed and filtered
329
+ * @param mapFn - Called once per own enumerable string key, with the value
330
+ * and the key
331
+ * @returns A new record holding only the kept entries, with mapped values
332
+ */
333
+ const filterMap: <const R extends UnknownRecord, V2>(record: R, mapFn: (value: R[keyof R], key: TsDataForgeInternals.MappedRecordKey<R>) => Optional<V2>) => TsDataForgeInternals.FilteredRecord<TsDataForgeInternals.MappedRecord<R, V2>>;
192
334
  /**
193
335
  * Merges multiple records into a single record using `Object.assign`.
194
336
  * Later records override properties from earlier records with the same key.
@@ -297,6 +439,36 @@ declare namespace TsDataForgeInternals {
297
439
  type ValuesOfEntries<Entries extends readonly (readonly [PropertyKey, unknown])[]> = ValuesOfEntriesImpl<never, Entries, RecursionLimit>;
298
440
  type ValuesOfEntriesImpl<K, Entries extends readonly (readonly [PropertyKey, unknown])[], RemainingNumRecursions extends number> = TypeEq<RemainingNumRecursions, 0> extends true ? K : TypeEq<Entries['length'], 0> extends true ? K : ValuesOfEntriesImpl<K | Entries[0][1], List.Tail<Entries>, Decrement<RemainingNumRecursions>>;
299
441
  type PartialIfKeyIsUnion<K, T> = IsUnion<K> extends true ? Partial<T> : T;
442
+ /**
443
+ * The keys `Object.entries` yields for `R`: symbol keys are not enumerated,
444
+ * and a numeric key `1` arrives as the string `'1'`.
445
+ */
446
+ type MappedRecordKey<R> = ToString<RelaxedExclude<keyof R, symbol>>;
447
+ /**
448
+ * `R` with every value replaced by `V2`. Homomorphic over `keyof R`, so
449
+ * optional modifiers and index signatures are preserved; the key remapping
450
+ * drops the symbol keys that `Object.entries` would not have visited.
451
+ */
452
+ type MappedRecord<R, V2> = Readonly<{
453
+ [K in keyof R as K extends symbol ? never : K]: V2;
454
+ }>;
455
+ /** `R` without the symbol keys `Object.entries` never visits. */
456
+ type StringKeyedRecord<R> = Readonly<{
457
+ [K in keyof R as K extends symbol ? never : K]: R[K];
458
+ }>;
459
+ /**
460
+ * The result of dropping entries from `R`: symbol keys go away, and the
461
+ * remaining keys become optional because a predicate may reject any of them.
462
+ *
463
+ * A record keyed by an index signature stays total instead. It names no
464
+ * specific key that could go missing, and `noUncheckedIndexedAccess` already
465
+ * adds `| undefined` on access, so making it `Partial` would add nothing and
466
+ * would only stop the result from being assignable back to the record type it
467
+ * came from.
468
+ */
469
+ type FilteredRecord<R> = string extends keyof R ? StringKeyedRecord<R> : number extends keyof R ? StringKeyedRecord<R> : Readonly<{
470
+ [K in keyof R as K extends symbol ? never : K]?: R[K];
471
+ }>;
300
472
  /**
301
473
  * Merges two object types where keys in B override keys in A, preserving optional modifiers.
302
474
  *
@@ -1 +1 @@
1
- {"version":3,"file":"object.d.mts","sourceRoot":"","sources":["../../src/object/object.mts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,OAAO,EACZ,KAAK,IAAI,EACT,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,eAAe,CAAC;AAIvB;;;;;;;GAOG;AACH,yBAAiB,GAAG,CAAC;IACnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyCG;IACI,MAAM,SAAS,MACjB,aAAa,KACb,aAAa,OACZ,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,KACtC,OAQF,CAAC;IAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA0CG;IACH,SAAgB,IAAI,CAClB,KAAK,CAAC,CAAC,SAAS,aAAa,EAC7B,KAAK,CAAC,IAAI,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EACvC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,UAAU,CAAC,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IAG5D,SAAgB,IAAI,CAAC,KAAK,CAAC,IAAI,SAAS,SAAS,WAAW,EAAE,EAC5D,IAAI,EAAE,IAAI,GACT,CAAC,KAAK,CAAC,CAAC,SAAS,aAAa,EAC/B,MAAM,EAAE,CAAC,KACN,WAAW,CAAC,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IAgCxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4CG;IACH,SAAgB,IAAI,CAClB,KAAK,CAAC,CAAC,SAAS,aAAa,EAC7B,KAAK,CAAC,IAAI,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EACvC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,UAAU,CAAC,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IAG5D,SAAgB,IAAI,CAAC,KAAK,CAAC,IAAI,SAAS,SAAS,WAAW,EAAE,EAC5D,IAAI,EAAE,IAAI,GACT,CAAC,KAAK,CAAC,CAAC,SAAS,aAAa,EAC/B,MAAM,EAAE,CAAC,KACN,WAAW,CAAC,CAAC,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IA0CxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4CG;IACI,MAAM,WAAW,GACtB,KAAK,CAAC,OAAO,SAAS,SAAS,CAAC,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,EAAE,WAEzD,OAAO,KACf,iBAAiB,CAAC,OAAO,CAAC,SAAS,IAAI,GACtC,oBAAoB,CAAC,eAAe,CAAC,OAAO,CAAC,GAC7C,oBAAoB,CAAC,mBAAmB,CACtC,oBAAoB,CAAC,aAAa,CAAC,OAAO,CAAC,EAC3C,cAAc,CACZ,oBAAoB,CAAC,aAAa,CAAC,OAAO,CAAC,EAC3C,oBAAoB,CAAC,eAAe,CAAC,OAAO,CAAC,CAC9C,CAG+B,CAAC;IAEvC;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACI,MAAM,KAAK,GAAI,KAAK,CAAC,OAAO,SAAS,SAAS,aAAa,EAAE,cACtD,OAAO,KAClB,oBAAoB,CAAC,QAAQ,CAAC,OAAO,CAEgC,CAAC;IAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,SAAgB,QAAQ,CACtB,KAAK,CAAC,CAAC,SAAS,aAAa,EAC7B,KAAK,CAAC,IAAI,SAAS,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EAC/C,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAG5C,SAAgB,QAAQ,CAAC,KAAK,CAAC,IAAI,SAAS,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EACtE,IAAI,EAAE,IAAI,GACT,CAAC,KAAK,CAAC,CAAC,SAAS,aAAa,EAAE,MAAM,EAAE,CAAC,KAAK,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IA0BnE;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,SAAgB,QAAQ,CACtB,KAAK,CAAC,CAAC,SAAS,aAAa,EAC7B,KAAK,CAAC,IAAI,SAAS,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EAC/C,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAG5C,SAAgB,QAAQ,CAAC,KAAK,CAAC,IAAI,SAAS,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EACtE,IAAI,EAAE,IAAI,GACT,CAAC,KAAK,CAAC,CAAC,SAAS,aAAa,EAAE,MAAM,EAAE,CAAC,KAAK,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;CAyBpE;AAqDD;;;GAGG;AACH,OAAO,WAAW,oBAAoB,CAAC;IACrC,KAAK,cAAc,GAAG,EAAE,CAAC;IAEzB,mDAAmD;IACnD,KAAY,eAAe,CACzB,OAAO,SAAS,SAAS,CAAC,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,EAAE,IAE1D,QAAQ,CAAC,mBAAmB,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;IAE/C,gBAAgB;IAChB,KAAK,mBAAmB,CACtB,CAAC,EACD,OAAO,SAAS,SAAS,CAAC,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,EAAE,IAE5D,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,IAAI,GACrC,CAAC,GACD,mBAAmB,CACjB,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAChD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CACnB,CAAC;IAER;;;;;;;OAOG;IACH,KAAY,aAAa,CACvB,OAAO,SAAS,SAAS,CAAC,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,EAAE,IAC1D,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;IAEtD,KAAK,iBAAiB,CACpB,CAAC,EACD,OAAO,SAAS,SAAS,CAAC,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,EAAE,EAC5D,sBAAsB,SAAS,MAAM,IAErC,MAAM,CAAC,sBAAsB,EAAE,CAAC,CAAC,SAAS,IAAI,GAC1C,CAAC,GACD,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,IAAI,GACvC,CAAC,GACD,iBAAiB,CACf,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EACjB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAClB,SAAS,CAAC,sBAAsB,CAAC,CAClC,CAAC;IAEV,KAAY,eAAe,CACzB,OAAO,SAAS,SAAS,CAAC,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,EAAE,IAC1D,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;IAExD,KAAK,mBAAmB,CACtB,CAAC,EACD,OAAO,SAAS,SAAS,CAAC,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,EAAE,EAC5D,sBAAsB,SAAS,MAAM,IAErC,MAAM,CAAC,sBAAsB,EAAE,CAAC,CAAC,SAAS,IAAI,GAC1C,CAAC,GACD,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,IAAI,GACvC,CAAC,GACD,mBAAmB,CACjB,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EACjB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAClB,SAAS,CAAC,sBAAsB,CAAC,CAClC,CAAC;IAEV,KAAY,mBAAmB,CAAC,CAAC,EAAE,CAAC,IAClC,OAAO,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCG;IAEH,KAAK,QAAQ,CAAC,CAAC,SAAS,aAAa,EAAE,CAAC,SAAS,aAAa,IAAI;QAGhE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,SAAS,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KACzE,GAAG;QAEF,QAAQ,EAEN,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,SAAS,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,GACvC,CAAC,SAAS,MAAM,CAAC,GACf,KAAK,GACL,CAAC,GACH,KAAK,CACV,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;KACT,GAAG;QAEF,QAAQ,EAEN,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,SAAS,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,GACvC,CAAC,SAAS,MAAM,CAAC,GACf,CAAC,GACD,KAAK,GACP,KAAK,CACV,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK;KAC5C,GAAG;QAEF,QAAQ,EAEN,CAAC,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,SAAS,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,GAChE,KAAK,GACL,CAAC,GACJ,CAAC,CAAC,CAAC,CAAC;KACR,GAAG;QAEF,QAAQ,EAEN,CAAC,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,SAAS,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,GAChE,CAAC,GACD,KAAK,CACV,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;KACT,SAAS,MAAM,CAAC,GACb,QAAQ,CACN;SAEG,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,SAAS,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KAChE,GAAG;SAED,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,SAAS,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;KACjE,CACF,GACD,KAAK,CAAC;IAEV,sEAAsE;IACtE,KAAY,QAAQ,CAClB,OAAO,SAAS,SAAS,aAAa,EAAE,EAExC,GAAG,SAAS,aAAa,GAAG,EAAE,IAE9B,iBAAiB,CAAC,OAAO,CAAC,SAAS,KAAK,GACpC,YAAY,CAAC,OAAO,CAAC,GACrB,OAAO,SAAS,SAAS;QACrB,MAAM,KAAK,SAAS,aAAa;QACjC,GAAG,MAAM,IAAI,SAAS,SAAS,aAAa,EAAE;KAC/C,GACD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,GACpC,GAAG,CAAC;CACb"}
1
+ {"version":3,"file":"object.d.mts","sourceRoot":"","sources":["../../src/object/object.mts"],"names":[],"mappings":"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"}
@@ -36,6 +36,7 @@ import '../number/num.mjs';
36
36
  import '../number/refined-number-utils.mjs';
37
37
  import { hasKey } from '../guard/has-key.mjs';
38
38
  import { isRecord } from '../guard/is-record.mjs';
39
+ import { isSome } from '../functional/optional/impl/optional-is-some.mjs';
39
40
  import '@sindresorhus/is';
40
41
  import { isNonEmpty } from '../array/impl/array-utils-length-bounded-array-guard.mjs';
41
42
  import '../collections/imap-mapped.mjs';
@@ -100,6 +101,48 @@ var Obj;
100
101
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
101
102
  Object.fromEntries(entries)
102
103
  );
104
+ Obj2.map = (record, mapFn) => {
105
+ const mapFnWidened = mapFn;
106
+ return (
107
+ // `Object.entries` enumerates exactly the own enumerable string keys of
108
+ // `record`, and nothing here adds or removes an entry, so the result has
109
+ // the key set `MappedRecord` claims.
110
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
111
+ Object.fromEntries(
112
+ Object.entries(record).map(
113
+ ([k, v]) => [k, mapFnWidened(v, k)]
114
+ )
115
+ )
116
+ );
117
+ };
118
+ function filter(record, predicate) {
119
+ const predicateWidened = predicate;
120
+ return (
121
+ // `Object.entries` enumerates exactly the own enumerable string keys of
122
+ // `record`, and filtering only ever removes entries, so the result has a
123
+ // subset of those keys — which is what `FilteredRecord` claims.
124
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
125
+ Object.fromEntries(
126
+ Object.entries(record).filter(([k, v]) => predicateWidened(v, k))
127
+ )
128
+ );
129
+ }
130
+ Obj2.filter = filter;
131
+ Obj2.filterMap = (record, mapFn) => {
132
+ const mapFnWidened = mapFn;
133
+ return (
134
+ // `Object.entries` enumerates exactly the own enumerable string keys of
135
+ // `record`, and this only ever drops entries or replaces their values, so
136
+ // the result has a subset of those keys with `V2` values.
137
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
138
+ Object.fromEntries(
139
+ Object.entries(record).flatMap(([k, v]) => {
140
+ const mapped = mapFnWidened(v, k);
141
+ return isSome(mapped) ? [[k, mapped.value]] : [];
142
+ })
143
+ )
144
+ );
145
+ };
103
146
  Obj2.merge = (...records) => (
104
147
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
105
148
  Object.fromEntries(records.flatMap((r) => Object.entries(r)))
@@ -1 +1 @@
1
- {"version":3,"file":"object.mjs","sources":["../../src/object/object.mts"],"sourcesContent":["import {\n type ArrayElement,\n type Decrement,\n type DeepOmit,\n type DeepPick,\n type IsFixedLengthList,\n type IsUnion,\n type List,\n type ReadonlyRecord,\n type RelaxedExclude,\n type RelaxedOmit,\n type RelaxedPick,\n type StrictOmit,\n type StrictPick,\n type TypeEq,\n type UnknownRecord,\n} from 'ts-type-forge';\nimport { Arr } from '../array/index.mjs';\nimport { hasKey, isRecord } from '../guard/index.mjs';\n\n/**\n * A collection of type-safe object utility functions providing functional\n * programming patterns for object manipulation, including pick, omit, shallow\n * equality checks, and more.\n *\n * All functions maintain TypeScript type safety and support both direct and\n * curried usage patterns for better composition with pipe operations.\n */\nexport namespace Obj {\n /**\n * Performs a shallow equality check on two records using a configurable\n * equality function. Verifies that both records have the same number of\n * entries and that for every key in the first record, the corresponding value\n * passes the equality test with the value in the second record.\n *\n * @example\n *\n * ```ts\n * const obj1 = { name: 'Alice', age: 30 } as const;\n *\n * const obj2 = { name: 'Alice', age: 30 } as const;\n *\n * const obj3 = { name: 'Alice', age: 31 } as const;\n *\n * assert.isTrue(Obj.shallowEq(obj1, obj2));\n *\n * assert.isFalse(Obj.shallowEq(obj1, obj3));\n *\n * // Custom equality function\n * const obj4 = { value: 1 } as const;\n *\n * const obj5 = { value: 1.00001 } as const;\n *\n * const closeEnough = (a: unknown, b: unknown): boolean => {\n * if (typeof a === 'number' && typeof b === 'number') {\n * return Math.abs(a - b) < 0.001;\n * }\n *\n * return Object.is(a, b);\n * };\n *\n * assert.isTrue(Obj.shallowEq(obj4, obj5, closeEnough));\n * ```\n *\n * @param a - The first record to compare\n * @param b - The second record to compare\n * @param eq - Optional equality function (defaults to Object.is for strict\n * equality)\n * @returns `true` if the records are shallowly equal according to the\n * equality function, `false` otherwise\n */\n export const shallowEq = (\n a: UnknownRecord,\n b: UnknownRecord,\n eq: (x: unknown, y: unknown) => boolean = Object.is,\n ): boolean => {\n const aEntries = Object.entries(a);\n\n const bEntries = Object.entries(b);\n\n if (aEntries.length !== bEntries.length) return false;\n\n return aEntries.every(([k, v]) => eq(b[k], v));\n };\n\n /**\n * Creates a new record that contains only the specified keys from the source\n * record. This function supports both direct usage and curried form for\n * functional composition.\n *\n * **Type Safety**: Only keys that exist in the source record type are\n * allowed, preventing runtime errors from accessing non-existent properties.\n *\n * @example\n *\n * ```ts\n * const user = {\n * id: 1,\n * name: 'Bob',\n * email: 'bob@example.com',\n * password: 'secret',\n * role: 'admin',\n * } as const;\n *\n * // Direct usage\n * const publicInfo = Obj.pick(user, ['id', 'name', 'role']);\n *\n * assert.deepStrictEqual(publicInfo, {\n * id: 1,\n * name: 'Bob',\n * role: 'admin',\n * });\n *\n * // Curried usage with pipe\n * const nameAndEmail = pipe(user).map(Obj.pick(['name', 'email'])).value;\n *\n * assert.deepStrictEqual(nameAndEmail, {\n * name: 'Bob',\n * email: 'bob@example.com',\n * });\n * ```\n *\n * @template R - The type of the input record\n * @template Keys - The readonly array type of keys to pick from the record\n * @param record - The source record to pick properties from\n * @param keys - A readonly array of keys to include in the result\n * @returns A new record containing only the specified keys and their values\n */\n export function pick<\n const R extends UnknownRecord,\n const Keys extends readonly (keyof R)[],\n >(record: R, keys: Keys): StrictPick<R, ArrayElement<Keys>>;\n\n // Curried version\n export function pick<const Keys extends readonly PropertyKey[]>(\n keys: Keys,\n ): <const R extends UnknownRecord>(\n record: R,\n ) => RelaxedPick<R, ArrayElement<Keys>>;\n\n export function pick<\n const R extends UnknownRecord,\n const Keys extends readonly (keyof R)[],\n >(\n ...args: readonly [record: R, keys: Keys] | readonly [keys: Keys]\n ):\n | StrictPick<R, ArrayElement<Keys>>\n | ((record: R) => RelaxedPick<R, ArrayElement<Keys>>) {\n switch (args.length) {\n case 2: {\n const [record, keys] = args;\n\n const keysSet = new Set<keyof R>(keys);\n\n return (\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(\n Object.entries(record).filter(([k, _v]) => keysSet.has(k)),\n ) as never\n );\n }\n\n case 1: {\n const [keys] = args;\n\n return (record: R) => pick(record, keys);\n }\n }\n }\n\n /**\n * Creates a new record that excludes the specified keys from the source\n * record. This function supports both direct usage and curried form for\n * functional composition.\n *\n * **Type Safety**: Only keys that exist in the source record type are\n * allowed, and the return type precisely reflects which properties remain\n * after omission.\n *\n * @example\n *\n * ```ts\n * const user = {\n * id: 1,\n * name: 'Charlie',\n * email: 'charlie@example.com',\n * password: 'secret123',\n * internalNote: 'VIP customer',\n * } as const;\n *\n * // Direct usage - remove sensitive fields\n * const safeUser = Obj.omit(user, ['password', 'internalNote']);\n *\n * assert.deepStrictEqual(safeUser, {\n * id: 1,\n * name: 'Charlie',\n * email: 'charlie@example.com',\n * });\n *\n * // Curried usage with pipe\n * const withoutEmail = pipe(user).map(Obj.omit(['email', 'password'])).value;\n *\n * assert.deepStrictEqual(withoutEmail, {\n * id: 1,\n * name: 'Charlie',\n * internalNote: 'VIP customer',\n * });\n * ```\n *\n * @template R - The type of the input record\n * @template Keys - The readonly array type of keys to omit from the record\n * @param record - The source record to omit properties from\n * @param keys - A readonly array of keys to exclude from the result\n * @returns A new record containing all properties except the specified keys\n */\n export function omit<\n const R extends UnknownRecord,\n const Keys extends readonly (keyof R)[],\n >(record: R, keys: Keys): StrictOmit<R, ArrayElement<Keys>>;\n\n // Curried version\n export function omit<const Keys extends readonly PropertyKey[]>(\n keys: Keys,\n ): <const R extends UnknownRecord>(\n record: R,\n ) => RelaxedOmit<R, ArrayElement<Keys>>;\n\n export function omit<\n const R extends UnknownRecord,\n const Keys extends readonly (keyof R)[],\n >(\n ...args:\n readonly [record: R, keys: Keys] | readonly [keys: readonly PropertyKey[]]\n ):\n | StrictOmit<R, ArrayElement<Keys>>\n | ((record: R) => StrictOmit<R, ArrayElement<Keys>>) {\n switch (args.length) {\n case 2: {\n const [record, keys] = args;\n\n const keysSet = new Set<keyof R>(keys);\n\n return (\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(\n Object.entries(record).filter(([k, _v]) => !keysSet.has(k)),\n ) as never\n );\n }\n\n case 1: {\n const [keys] = args;\n\n return <R2 extends UnknownRecord>(record: R2) => {\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n const result = omit(\n record,\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n keys as readonly (keyof R2)[],\n ) as RelaxedOmit<R2, ArrayElement<Keys>>;\n\n return result;\n };\n }\n }\n }\n\n /**\n * Creates an object from an array of key-value pairs with precise TypeScript\n * typing. This is a type-safe wrapper around `Object.fromEntries` that\n * provides better type inference and compile-time guarantees about the\n * resulting object structure.\n *\n * **Type Behavior**:\n *\n * - When entries is a fixed-length tuple, the exact object type is inferred\n * - When entries has dynamic length with union key types, `Partial` is applied\n * to prevent incorrect assumptions about which keys will be present\n *\n * @example\n *\n * ```ts\n * // Fixed-length tuple - exact type inferred\n * const entries1 = [\n * ['name', 'David'],\n * ['age', 25],\n * ['active', true],\n * ] as const;\n *\n * const obj1 = Obj.fromEntries(entries1);\n *\n * assert.deepStrictEqual(obj1, {\n * name: 'David',\n * age: 25,\n * active: true,\n * });\n *\n * // Dynamic length array - Partial type applied\n * const dynamicEntries: readonly (readonly ['x' | 'y', number])[] = [\n * ['x', 10],\n * ['y', 20],\n * ] as const;\n *\n * const obj2 = Obj.fromEntries(dynamicEntries);\n *\n * assert.deepStrictEqual(obj2, { x: 10, y: 20 });\n * ```\n *\n * @template Entries - The readonly array type of key-value entry tuples\n * @param entries - An array of readonly key-value entry tuples `[key, value]`\n * @returns An object created from the entries with precise typing\n */\n export const fromEntries = <\n const Entries extends readonly (readonly [PropertyKey, unknown])[],\n >(\n entries: Entries,\n ): IsFixedLengthList<Entries> extends true\n ? TsDataForgeInternals.EntriesToObject<Entries>\n : TsDataForgeInternals.PartialIfKeyIsUnion<\n TsDataForgeInternals.KeysOfEntries<Entries>,\n ReadonlyRecord<\n TsDataForgeInternals.KeysOfEntries<Entries>,\n TsDataForgeInternals.ValuesOfEntries<Entries>\n >\n > =>\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(entries) as never;\n\n /**\n * Merges multiple records into a single record using `Object.assign`.\n * Later records override properties from earlier records with the same key.\n *\n * @example\n *\n * <!-- doc:embed:jsdoc:example:./samples/src/object/merge-example.mts -->\n *\n * ```ts\n * const a = { a: 0, b: 0 } as const;\n *\n * const b = { b: 1, c: 0 } as const;\n *\n * const result = Obj.merge(a, b);\n *\n * assert.deepStrictEqual(result, { a: 0, b: 1, c: 0 });\n * ```\n *\n * <!-- /doc:embed:jsdoc:example:./samples/src/object/merge-example.mts -->\n *\n * @param records - The records to merge\n * @returns A new record with all properties merged\n */\n export const merge = <const Records extends readonly UnknownRecord[]>(\n ...records: Records\n ): TsDataForgeInternals.MergeAll<Records> =>\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(records.flatMap((r) => Object.entries(r))) as never;\n\n /**\n * Deeply picks a nested property from an object along the specified key path.\n * Supports both direct and curried usage.\n *\n * @example\n *\n * ```ts\n * const data = { a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 } as const;\n *\n * // Direct usage\n * const result = Obj.deepPick(data, ['a', 'b', 'c']);\n *\n * assert.deepStrictEqual(result, { a: { b: { c: 1 } } });\n *\n * // Curried usage with pipe\n * const pickAB = Obj.deepPick(['a', 'b']);\n *\n * const result2 = pipe(data).map(pickAB).value;\n *\n * assert.deepStrictEqual(result2, { a: { b: { c: 1, d: 2 } } });\n * ```\n *\n * @template R - The type of the input record\n * @template Path - The key path tuple\n * @param record - The source record\n * @param path - A readonly tuple of keys representing the nested path\n * @returns A new record containing only the nested property at the path\n */\n export function deepPick<\n const R extends UnknownRecord,\n const Path extends readonly (string | number)[],\n >(record: R, path: Path): DeepPick<R, Path>;\n\n // Curried version\n export function deepPick<const Path extends readonly (string | number)[]>(\n path: Path,\n ): <const R extends UnknownRecord>(record: R) => DeepPick<R, Path>;\n\n export function deepPick<\n const R extends UnknownRecord,\n const Path extends readonly (string | number)[],\n >(\n ...args: readonly [record: R, path: Path] | readonly [path: Path]\n ): DeepPick<R, Path> | ((record: R) => DeepPick<R, Path>) {\n switch (args.length) {\n case 2: {\n const [record, path] = args;\n\n return (\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n deepPickImpl(record, path) as never\n );\n }\n\n case 1: {\n const [path] = args;\n\n return (record: R) => deepPick(record, path);\n }\n }\n }\n\n /**\n * Deeply omits a nested property from an object along the specified key path.\n * Supports both direct and curried usage.\n *\n * @example\n *\n * ```ts\n * const data = { a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 } as const;\n *\n * // Direct usage\n * const result = Obj.deepOmit(data, ['a', 'b', 'c']);\n *\n * assert.deepStrictEqual(result, { a: { b: { d: 2 }, e: 3 }, f: 4 });\n *\n * // Curried usage with pipe\n * const omitAB = Obj.deepOmit(['a', 'b']);\n *\n * const result2 = pipe(data).map(omitAB).value;\n *\n * assert.deepStrictEqual(result2, { a: { e: 3 }, f: 4 });\n * ```\n *\n * @template R - The type of the input record\n * @template Path - The key path tuple\n * @param record - The source record\n * @param path - A readonly tuple of keys representing the nested path to omit\n * @returns A new record with the nested property at the path removed\n */\n export function deepOmit<\n const R extends UnknownRecord,\n const Path extends readonly (string | number)[],\n >(record: R, path: Path): DeepOmit<R, Path>;\n\n // Curried version\n export function deepOmit<const Path extends readonly (string | number)[]>(\n path: Path,\n ): <const R extends UnknownRecord>(record: R) => DeepOmit<R, Path>;\n\n export function deepOmit<\n const R extends UnknownRecord,\n const Path extends readonly (string | number)[],\n >(\n ...args: readonly [record: R, path: Path] | readonly [path: Path]\n ): DeepOmit<R, Path> | ((record: R) => DeepOmit<R, Path>) {\n switch (args.length) {\n case 2: {\n const [record, path] = args;\n\n return (\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n deepOmitImpl(record, path) as never\n );\n }\n\n case 1: {\n const [path] = args;\n\n return (record: R) => deepOmit(record, path);\n }\n }\n }\n}\n\nconst deepPickImpl = (\n record: UnknownRecord,\n path: readonly (string | number)[],\n): UnknownRecord => {\n if (!Arr.isNonEmpty(path)) return record;\n\n const head = path[0];\n\n if (!hasKey(record, head)) return {};\n\n const value = record[head];\n\n const tail = path.slice(1);\n\n if (!Arr.isNonEmpty(tail)) return { [head]: value };\n\n if (!isRecord(value)) return { [head]: {} };\n\n return {\n [head]: deepPickImpl(value, tail),\n };\n};\n\nconst deepOmitImpl = (\n record: UnknownRecord,\n path: readonly (string | number)[],\n): UnknownRecord => {\n if (!Arr.isNonEmpty(path)) return record;\n\n const head = path[0];\n\n if (!hasKey(record, head)) return record;\n\n const tail = path.slice(1);\n\n if (!Arr.isNonEmpty(tail)) {\n return Object.fromEntries(\n Object.entries(record).filter(([k]) => k !== head),\n );\n }\n\n const value = record[head];\n\n if (!isRecord(value)) return record;\n\n return {\n ...record,\n [head]: deepOmitImpl(value, tail),\n };\n};\n\n/**\n * @internal\n * Internal type utilities for the Obj module.\n */\ndeclare namespace TsDataForgeInternals {\n type RecursionLimit = 20;\n\n /** - `[['x', 1], ['y', 3]]` -> `{ x: 1, y: 3 }` */\n export type EntriesToObject<\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n > = Readonly<EntriesToObjectImpl<{}, Entries>>;\n\n /** @internal */\n type EntriesToObjectImpl<\n R,\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n > =\n TypeEq<Entries['length'], 0> extends true\n ? R\n : EntriesToObjectImpl<\n R & ReadonlyRecord<Entries[0][0], Entries[0][1]>,\n List.Tail<Entries>\n >;\n\n /**\n * - `['x' | 'y' | 'z', number][]]` -> `'x' | 'y' | 'z'`\n * - `[['a' | 'b' | 'c', number], ...['x' | 'y' | 'z', number][]]` -> `'a' |\n * 'b' | 'c' | 'x' | 'y' | 'z'`\n *\n * @note To handle the second example above, recursion needs to be performed on infinite-length Entries,\n * but since the timing to stop cannot be determined, a recursion limit is set.\n */\n export type KeysOfEntries<\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n > = KeysOfEntriesImpl<never, Entries, RecursionLimit>;\n\n type KeysOfEntriesImpl<\n K,\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n RemainingNumRecursions extends number,\n > =\n TypeEq<RemainingNumRecursions, 0> extends true\n ? K\n : TypeEq<Entries['length'], 0> extends true\n ? K\n : KeysOfEntriesImpl<\n K | Entries[0][0],\n List.Tail<Entries>,\n Decrement<RemainingNumRecursions>\n >;\n\n export type ValuesOfEntries<\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n > = ValuesOfEntriesImpl<never, Entries, RecursionLimit>;\n\n type ValuesOfEntriesImpl<\n K,\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n RemainingNumRecursions extends number,\n > =\n TypeEq<RemainingNumRecursions, 0> extends true\n ? K\n : TypeEq<Entries['length'], 0> extends true\n ? K\n : ValuesOfEntriesImpl<\n K | Entries[0][1],\n List.Tail<Entries>,\n Decrement<RemainingNumRecursions>\n >;\n\n export type PartialIfKeyIsUnion<K, T> =\n IsUnion<K> extends true ? Partial<T> : T;\n\n /**\n * Merges two object types where keys in B override keys in A, preserving optional modifiers.\n *\n * This implementation uses a sophisticated technique to preserve optional property modifiers (`?`)\n * during the merge operation, which would otherwise be lost in a naive mapped type implementation.\n *\n * ## Core Technique: Optional Property Detection\n *\n * Uses `{} extends Pick<T, K>` to determine if a property is optional:\n * - **Required property**: `Pick<T, 'x'>` = `{ x: number }` → `{}` does NOT extend it (false)\n * - **Optional property**: `Pick<T, 'y'>` = `{ y?: string }` → `{}` DOES extend it (true)\n *\n * This works because `{}` (empty object) is assignable to objects with only optional properties\n * but not to objects with required properties.\n *\n * ## Implementation Strategy\n *\n * The type is constructed as an intersection of five mapped types:\n *\n * 1. **Required properties from B** - Properties in B that are required (override A completely)\n * 2. **Optional properties from B (not in A)** - New optional properties from B\n * 3. **Optional properties from B (in A)** - Union of A[K] | B[K] to preserve both possibilities\n * 4. **Required properties only in A** - Properties in A but not in B that are required\n * 5. **Optional properties only in A** - Properties in A but not in B that are optional (with `?` modifier)\n *\n * Finally, the intersection is flattened using an optional-preserving identity mapping that\n * re-applies the optional detection logic to maintain the `?` modifiers in the final type.\n *\n * @example\n * ```ts\n * type A = Readonly<{ x: number; y?: string }>;\n *\n * type B = Readonly<{ y?: number; z?: boolean }>;\n *\n * type Result = MergeTwo<A, B>;\n * // Result: Readonly<{ x: number; y?: string | number; z?: boolean }>\n * ```\n */\n // transformer-ignore-next-line\n type MergeTwo<A extends UnknownRecord, B extends UnknownRecord> = {\n // 1. Required properties from B (override A completely)\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n readonly [K in keyof B as {} extends StrictPick<B, K> ? never : K]: B[K];\n } & {\n // 2. Optional properties from B that are NOT in A\n readonly [\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n K in keyof B as {} extends StrictPick<B, K>\n ? K extends keyof A\n ? never\n : K\n : never\n ]?: B[K];\n } & {\n // 3. Optional properties from B that ARE in A (create union)\n readonly [\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n K in keyof B as {} extends StrictPick<B, K>\n ? K extends keyof A\n ? K\n : never\n : never\n ]?: K extends keyof A ? A[K] | B[K] : never;\n } & {\n // 4. Required properties only in A (not overridden by B)\n readonly [\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n K in RelaxedExclude<keyof A, keyof B> as {} extends StrictPick<A, K>\n ? never\n : K\n ]: A[K];\n } & {\n // 5. Optional properties only in A (not overridden by B)\n readonly [\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n K in RelaxedExclude<keyof A, keyof B> as {} extends StrictPick<A, K>\n ? K\n : never\n ]?: A[K];\n } extends infer O\n ? Readonly<\n {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n [K in keyof O as {} extends StrictPick<O, K> ? never : K]: O[K];\n } & {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n [K in keyof O as {} extends StrictPick<O, K> ? K : never]?: O[K];\n }\n >\n : never;\n\n /** Sequentially merges a tuple of object types from left to right. */\n export type MergeAll<\n Records extends readonly UnknownRecord[],\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n Acc extends UnknownRecord = {},\n > =\n IsFixedLengthList<Records> extends false\n ? ArrayElement<Records>\n : Records extends readonly [\n infer First extends UnknownRecord,\n ...infer Rest extends readonly UnknownRecord[],\n ]\n ? MergeAll<Rest, MergeTwo<Acc, First>>\n : Acc;\n}\n"],"names":["Obj","Arr.isNonEmpty"],"mappings":"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"}
1
+ {"version":3,"file":"object.mjs","sources":["../../src/object/object.mts"],"sourcesContent":["import {\n type ArrayElement,\n type Decrement,\n type DeepOmit,\n type DeepPick,\n type IsFixedLengthList,\n type IsUnion,\n type List,\n type ReadonlyRecord,\n type RelaxedExclude,\n type RelaxedOmit,\n type RelaxedPick,\n type StrictOmit,\n type StrictPick,\n type ToString,\n type TypeEq,\n type UnknownRecord,\n} from 'ts-type-forge';\nimport { Arr } from '../array/index.mjs';\nimport { Optional } from '../functional/index.mjs';\nimport { hasKey, isRecord } from '../guard/index.mjs';\n\n/**\n * A collection of type-safe object utility functions providing functional\n * programming patterns for object manipulation, including pick, omit, shallow\n * equality checks, and more.\n *\n * All functions maintain TypeScript type safety and support both direct and\n * curried usage patterns for better composition with pipe operations.\n */\nexport namespace Obj {\n /**\n * Performs a shallow equality check on two records using a configurable\n * equality function. Verifies that both records have the same number of\n * entries and that for every key in the first record, the corresponding value\n * passes the equality test with the value in the second record.\n *\n * @example\n *\n * ```ts\n * const obj1 = { name: 'Alice', age: 30 } as const;\n *\n * const obj2 = { name: 'Alice', age: 30 } as const;\n *\n * const obj3 = { name: 'Alice', age: 31 } as const;\n *\n * assert.isTrue(Obj.shallowEq(obj1, obj2));\n *\n * assert.isFalse(Obj.shallowEq(obj1, obj3));\n *\n * // Custom equality function\n * const obj4 = { value: 1 } as const;\n *\n * const obj5 = { value: 1.00001 } as const;\n *\n * const closeEnough = (a: unknown, b: unknown): boolean => {\n * if (typeof a === 'number' && typeof b === 'number') {\n * return Math.abs(a - b) < 0.001;\n * }\n *\n * return Object.is(a, b);\n * };\n *\n * assert.isTrue(Obj.shallowEq(obj4, obj5, closeEnough));\n * ```\n *\n * @param a - The first record to compare\n * @param b - The second record to compare\n * @param eq - Optional equality function (defaults to Object.is for strict\n * equality)\n * @returns `true` if the records are shallowly equal according to the\n * equality function, `false` otherwise\n */\n export const shallowEq = (\n a: UnknownRecord,\n b: UnknownRecord,\n eq: (x: unknown, y: unknown) => boolean = Object.is,\n ): boolean => {\n const aEntries = Object.entries(a);\n\n const bEntries = Object.entries(b);\n\n if (aEntries.length !== bEntries.length) return false;\n\n return aEntries.every(([k, v]) => eq(b[k], v));\n };\n\n /**\n * Creates a new record that contains only the specified keys from the source\n * record. This function supports both direct usage and curried form for\n * functional composition.\n *\n * **Type Safety**: Only keys that exist in the source record type are\n * allowed, preventing runtime errors from accessing non-existent properties.\n *\n * @example\n *\n * ```ts\n * const user = {\n * id: 1,\n * name: 'Bob',\n * email: 'bob@example.com',\n * password: 'secret',\n * role: 'admin',\n * } as const;\n *\n * // Direct usage\n * const publicInfo = Obj.pick(user, ['id', 'name', 'role']);\n *\n * assert.deepStrictEqual(publicInfo, {\n * id: 1,\n * name: 'Bob',\n * role: 'admin',\n * });\n *\n * // Curried usage with pipe\n * const nameAndEmail = pipe(user).map(Obj.pick(['name', 'email'])).value;\n *\n * assert.deepStrictEqual(nameAndEmail, {\n * name: 'Bob',\n * email: 'bob@example.com',\n * });\n * ```\n *\n * @template R - The type of the input record\n * @template Keys - The readonly array type of keys to pick from the record\n * @param record - The source record to pick properties from\n * @param keys - A readonly array of keys to include in the result\n * @returns A new record containing only the specified keys and their values\n */\n export function pick<\n const R extends UnknownRecord,\n const Keys extends readonly (keyof R)[],\n >(record: R, keys: Keys): StrictPick<R, ArrayElement<Keys>>;\n\n // Curried version\n export function pick<const Keys extends readonly PropertyKey[]>(\n keys: Keys,\n ): <const R extends UnknownRecord>(\n record: R,\n ) => RelaxedPick<R, ArrayElement<Keys>>;\n\n export function pick<\n const R extends UnknownRecord,\n const Keys extends readonly (keyof R)[],\n >(\n ...args: readonly [record: R, keys: Keys] | readonly [keys: Keys]\n ):\n | StrictPick<R, ArrayElement<Keys>>\n | ((record: R) => RelaxedPick<R, ArrayElement<Keys>>) {\n switch (args.length) {\n case 2: {\n const [record, keys] = args;\n\n const keysSet = new Set<keyof R>(keys);\n\n return (\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(\n Object.entries(record).filter(([k, _v]) => keysSet.has(k)),\n ) as never\n );\n }\n\n case 1: {\n const [keys] = args;\n\n return (record: R) => pick(record, keys);\n }\n }\n }\n\n /**\n * Creates a new record that excludes the specified keys from the source\n * record. This function supports both direct usage and curried form for\n * functional composition.\n *\n * **Type Safety**: Only keys that exist in the source record type are\n * allowed, and the return type precisely reflects which properties remain\n * after omission.\n *\n * @example\n *\n * ```ts\n * const user = {\n * id: 1,\n * name: 'Charlie',\n * email: 'charlie@example.com',\n * password: 'secret123',\n * internalNote: 'VIP customer',\n * } as const;\n *\n * // Direct usage - remove sensitive fields\n * const safeUser = Obj.omit(user, ['password', 'internalNote']);\n *\n * assert.deepStrictEqual(safeUser, {\n * id: 1,\n * name: 'Charlie',\n * email: 'charlie@example.com',\n * });\n *\n * // Curried usage with pipe\n * const withoutEmail = pipe(user).map(Obj.omit(['email', 'password'])).value;\n *\n * assert.deepStrictEqual(withoutEmail, {\n * id: 1,\n * name: 'Charlie',\n * internalNote: 'VIP customer',\n * });\n * ```\n *\n * @template R - The type of the input record\n * @template Keys - The readonly array type of keys to omit from the record\n * @param record - The source record to omit properties from\n * @param keys - A readonly array of keys to exclude from the result\n * @returns A new record containing all properties except the specified keys\n */\n export function omit<\n const R extends UnknownRecord,\n const Keys extends readonly (keyof R)[],\n >(record: R, keys: Keys): StrictOmit<R, ArrayElement<Keys>>;\n\n // Curried version\n export function omit<const Keys extends readonly PropertyKey[]>(\n keys: Keys,\n ): <const R extends UnknownRecord>(\n record: R,\n ) => RelaxedOmit<R, ArrayElement<Keys>>;\n\n export function omit<\n const R extends UnknownRecord,\n const Keys extends readonly (keyof R)[],\n >(\n ...args:\n readonly [record: R, keys: Keys] | readonly [keys: readonly PropertyKey[]]\n ):\n | StrictOmit<R, ArrayElement<Keys>>\n | ((record: R) => StrictOmit<R, ArrayElement<Keys>>) {\n switch (args.length) {\n case 2: {\n const [record, keys] = args;\n\n const keysSet = new Set<keyof R>(keys);\n\n return (\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(\n Object.entries(record).filter(([k, _v]) => !keysSet.has(k)),\n ) as never\n );\n }\n\n case 1: {\n const [keys] = args;\n\n return <R2 extends UnknownRecord>(record: R2) => {\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n const result = omit(\n record,\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n keys as readonly (keyof R2)[],\n ) as RelaxedOmit<R2, ArrayElement<Keys>>;\n\n return result;\n };\n }\n }\n }\n\n /**\n * Creates an object from an array of key-value pairs with precise TypeScript\n * typing. This is a type-safe wrapper around `Object.fromEntries` that\n * provides better type inference and compile-time guarantees about the\n * resulting object structure.\n *\n * **Type Behavior**:\n *\n * - When entries is a fixed-length tuple, the exact object type is inferred\n * - When entries has dynamic length with union key types, `Partial` is applied\n * to prevent incorrect assumptions about which keys will be present\n *\n * @example\n *\n * ```ts\n * // Fixed-length tuple - exact type inferred\n * const entries1 = [\n * ['name', 'David'],\n * ['age', 25],\n * ['active', true],\n * ] as const;\n *\n * const obj1 = Obj.fromEntries(entries1);\n *\n * assert.deepStrictEqual(obj1, {\n * name: 'David',\n * age: 25,\n * active: true,\n * });\n *\n * // Dynamic length array - Partial type applied\n * const dynamicEntries: readonly (readonly ['x' | 'y', number])[] = [\n * ['x', 10],\n * ['y', 20],\n * ] as const;\n *\n * const obj2 = Obj.fromEntries(dynamicEntries);\n *\n * assert.deepStrictEqual(obj2, { x: 10, y: 20 });\n * ```\n *\n * @template Entries - The readonly array type of key-value entry tuples\n * @param entries - An array of readonly key-value entry tuples `[key, value]`\n * @returns An object created from the entries with precise typing\n */\n export const fromEntries = <\n const Entries extends readonly (readonly [PropertyKey, unknown])[],\n >(\n entries: Entries,\n ): IsFixedLengthList<Entries> extends true\n ? TsDataForgeInternals.EntriesToObject<Entries>\n : TsDataForgeInternals.PartialIfKeyIsUnion<\n TsDataForgeInternals.KeysOfEntries<Entries>,\n ReadonlyRecord<\n TsDataForgeInternals.KeysOfEntries<Entries>,\n TsDataForgeInternals.ValuesOfEntries<Entries>\n >\n > =>\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(entries) as never;\n\n /**\n * Transforms every value of a record, leaving the key set untouched.\n *\n * **Type Behavior**: the result is a mapped type over `keyof R`, so the keys\n * — and their optional modifiers — survive verbatim:\n * `{ a: number; b?: number }` maps to `{ a: V2; b?: V2 }`, and a record with\n * an index signature stays total rather than becoming `Partial`.\n *\n * This is what `Object.fromEntries(Object.entries(record).map(...))` cannot\n * express: an entries array carries no evidence that it still covers every\n * key of the record it came from, so its element type is all the return type\n * can be built from. Going through a mapped type instead states the\n * invariant that a value-only transform preserves the key set.\n *\n * **Key handling**: keys reach the callback the way `Object.entries` yields\n * them, so a numeric key `1` arrives as `'1'`. Symbol-keyed properties are\n * not enumerated by `Object.entries`; they are dropped from both the result\n * and its type.\n *\n * @example\n *\n * <!-- doc:embed:jsdoc:example:./samples/src/object/map-example.mts -->\n *\n * ```ts\n * const scores = { alice: 1, bob: 2 } as const;\n *\n * const doubled = Obj.map(scores, (value) => value * 2);\n *\n * assert.deepStrictEqual(doubled, { alice: 2, bob: 4 });\n *\n * // The key is available as the second argument.\n * const labelled = Obj.map(scores, (value, key) => `${key}=${value}`);\n *\n * assert.deepStrictEqual(labelled, { alice: 'alice=1', bob: 'bob=2' });\n * ```\n *\n * <!-- /doc:embed:jsdoc:example:./samples/src/object/map-example.mts -->\n *\n * @template R - The type of the source record\n * @template V2 - The type the callback returns for every key\n * @param record - The record whose values are transformed\n * @param mapFn - Called once per own enumerable string key, with the value\n * and the key\n * @returns A new record with the same keys as `record` and the mapped values\n */\n export const map = <const R extends UnknownRecord, V2>(\n record: R,\n mapFn: (\n value: R[keyof R],\n key: TsDataForgeInternals.MappedRecordKey<R>,\n ) => V2,\n ): TsDataForgeInternals.MappedRecord<R, V2> => {\n // `Object.entries` erases both the key and the value type of `R`, so the\n // callback is widened once here instead of asserting on every entry.\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n const mapFnWidened = mapFn as (value: unknown, key: string) => V2;\n\n return (\n // `Object.entries` enumerates exactly the own enumerable string keys of\n // `record`, and nothing here adds or removes an entry, so the result has\n // the key set `MappedRecord` claims.\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(\n Object.entries(record).map(\n ([k, v]) => [k, mapFnWidened(v, k)] as const,\n ),\n ) as never\n );\n };\n\n /**\n * Keeps the entries whose value the predicate accepts, leaving the values\n * themselves untouched.\n *\n * **Type Behavior**: the surviving keys are the keys of the source record,\n * made optional — the predicate may reject any of them. A record keyed by an\n * index signature stays total instead, because it names no specific key that\n * could go missing.\n *\n * Passing a type guard narrows the value type of the result, which is what\n * `Object.fromEntries(Object.entries(record).filter(guard))` loses.\n *\n * **Key handling**: keys reach the callback the way `Object.entries` yields\n * them, so a numeric key `1` arrives as `'1'`. Symbol-keyed properties are\n * not enumerated by `Object.entries`; they are dropped from both the result\n * and its type.\n *\n * @example\n *\n * <!-- doc:embed:jsdoc:example:./samples/src/object/filter-example.mts -->\n *\n * ```ts\n * const scores = { alice: 1, bob: 2 } as const;\n *\n * const passing = Obj.filter(scores, (value) => value > 1);\n *\n * assert.deepStrictEqual(passing, { bob: 2 });\n *\n * // The key is available as the second argument.\n * const notAlice = Obj.filter(scores, (_value, key) => key !== 'alice');\n *\n * assert.deepStrictEqual(notAlice, { bob: 2 });\n *\n * // A type guard narrows the value type of the result.\n * const mixed: UnknownRecord = { a: 1, b: 'x' } as const;\n *\n * const strings = Obj.filter(mixed, isString);\n *\n * assert.deepStrictEqual(strings, { b: 'x' });\n * ```\n *\n * <!-- /doc:embed:jsdoc:example:./samples/src/object/filter-example.mts -->\n *\n * @template R - The type of the source record\n * @template V2 - The type the guard narrows values to\n * @param record - The record whose entries are filtered\n * @param predicate - Called once per own enumerable string key, with the\n * value and the key\n * @returns A new record holding only the accepted entries\n */\n export function filter<const R extends UnknownRecord, V2 extends R[keyof R]>(\n record: R,\n predicate: (\n value: R[keyof R],\n key: TsDataForgeInternals.MappedRecordKey<R>,\n ) => value is V2,\n ): TsDataForgeInternals.FilteredRecord<\n TsDataForgeInternals.MappedRecord<R, V2>\n >;\n\n export function filter<const R extends UnknownRecord>(\n record: R,\n predicate: (\n value: R[keyof R],\n key: TsDataForgeInternals.MappedRecordKey<R>,\n ) => boolean,\n ): TsDataForgeInternals.FilteredRecord<R>;\n\n export function filter<const R extends UnknownRecord>(\n record: R,\n predicate: (\n value: R[keyof R],\n key: TsDataForgeInternals.MappedRecordKey<R>,\n ) => boolean,\n ): TsDataForgeInternals.FilteredRecord<R> {\n // `Object.entries` erases both the key and the value type of `R`, so the\n // callback is widened once here instead of asserting on every entry.\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n const predicateWidened = predicate as (\n value: unknown,\n key: string,\n ) => boolean;\n\n return (\n // `Object.entries` enumerates exactly the own enumerable string keys of\n // `record`, and filtering only ever removes entries, so the result has a\n // subset of those keys — which is what `FilteredRecord` claims.\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(\n Object.entries(record).filter(([k, v]) => predicateWidened(v, k)),\n ) as never\n );\n }\n\n /**\n * Transforms and drops entries in one pass: the callback returns\n * `Optional.some(value)` to keep an entry with a new value, or\n * `Optional.none` to drop it.\n *\n * **Type Behavior**: as with {@link filter}, the surviving keys are the keys\n * of the source record made optional, and a record keyed by an index\n * signature stays total. Unlike {@link filter}, every kept value takes the\n * type the callback returns.\n *\n * `Optional` rather than `undefined` marks the dropped entries, so a record\n * whose values are legitimately `undefined` stays expressible.\n *\n * **Key handling**: keys reach the callback the way `Object.entries` yields\n * them, so a numeric key `1` arrives as `'1'`. Symbol-keyed properties are\n * not enumerated by `Object.entries`; they are dropped from both the result\n * and its type.\n *\n * @example\n *\n * <!-- doc:embed:jsdoc:example:./samples/src/object/filter-map-example.mts -->\n *\n * ```ts\n * const scores = { alice: 1, bob: 2 } as const;\n *\n * // `Optional.some` keeps an entry with a new value, `Optional.none` drops it.\n * const labelled = Obj.filterMap(scores, (value, key) =>\n * value > 1 ? Optional.some(`${key}=${value}`) : Optional.none,\n * );\n *\n * assert.deepStrictEqual(labelled, { bob: 'bob=2' });\n * ```\n *\n * <!-- /doc:embed:jsdoc:example:./samples/src/object/filter-map-example.mts -->\n *\n * @template R - The type of the source record\n * @template V2 - The type the callback returns for the entries it keeps\n * @param record - The record whose entries are transformed and filtered\n * @param mapFn - Called once per own enumerable string key, with the value\n * and the key\n * @returns A new record holding only the kept entries, with mapped values\n */\n export const filterMap = <const R extends UnknownRecord, V2>(\n record: R,\n mapFn: (\n value: R[keyof R],\n key: TsDataForgeInternals.MappedRecordKey<R>,\n ) => Optional<V2>,\n ): TsDataForgeInternals.FilteredRecord<\n TsDataForgeInternals.MappedRecord<R, V2>\n > => {\n // `Object.entries` erases both the key and the value type of `R`, so the\n // callback is widened once here instead of asserting on every entry.\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n const mapFnWidened = mapFn as (value: unknown, key: string) => Optional<V2>;\n\n return (\n // `Object.entries` enumerates exactly the own enumerable string keys of\n // `record`, and this only ever drops entries or replaces their values, so\n // the result has a subset of those keys with `V2` values.\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(\n Object.entries(record).flatMap(([k, v]) => {\n const mapped = mapFnWidened(v, k);\n\n return Optional.isSome(mapped) ? [[k, mapped.value] as const] : [];\n }),\n ) as never\n );\n };\n\n /**\n * Merges multiple records into a single record using `Object.assign`.\n * Later records override properties from earlier records with the same key.\n *\n * @example\n *\n * <!-- doc:embed:jsdoc:example:./samples/src/object/merge-example.mts -->\n *\n * ```ts\n * const a = { a: 0, b: 0 } as const;\n *\n * const b = { b: 1, c: 0 } as const;\n *\n * const result = Obj.merge(a, b);\n *\n * assert.deepStrictEqual(result, { a: 0, b: 1, c: 0 });\n * ```\n *\n * <!-- /doc:embed:jsdoc:example:./samples/src/object/merge-example.mts -->\n *\n * @param records - The records to merge\n * @returns A new record with all properties merged\n */\n export const merge = <const Records extends readonly UnknownRecord[]>(\n ...records: Records\n ): TsDataForgeInternals.MergeAll<Records> =>\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n Object.fromEntries(records.flatMap((r) => Object.entries(r))) as never;\n\n /**\n * Deeply picks a nested property from an object along the specified key path.\n * Supports both direct and curried usage.\n *\n * @example\n *\n * ```ts\n * const data = { a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 } as const;\n *\n * // Direct usage\n * const result = Obj.deepPick(data, ['a', 'b', 'c']);\n *\n * assert.deepStrictEqual(result, { a: { b: { c: 1 } } });\n *\n * // Curried usage with pipe\n * const pickAB = Obj.deepPick(['a', 'b']);\n *\n * const result2 = pipe(data).map(pickAB).value;\n *\n * assert.deepStrictEqual(result2, { a: { b: { c: 1, d: 2 } } });\n * ```\n *\n * @template R - The type of the input record\n * @template Path - The key path tuple\n * @param record - The source record\n * @param path - A readonly tuple of keys representing the nested path\n * @returns A new record containing only the nested property at the path\n */\n export function deepPick<\n const R extends UnknownRecord,\n const Path extends readonly (string | number)[],\n >(record: R, path: Path): DeepPick<R, Path>;\n\n // Curried version\n export function deepPick<const Path extends readonly (string | number)[]>(\n path: Path,\n ): <const R extends UnknownRecord>(record: R) => DeepPick<R, Path>;\n\n export function deepPick<\n const R extends UnknownRecord,\n const Path extends readonly (string | number)[],\n >(\n ...args: readonly [record: R, path: Path] | readonly [path: Path]\n ): DeepPick<R, Path> | ((record: R) => DeepPick<R, Path>) {\n switch (args.length) {\n case 2: {\n const [record, path] = args;\n\n return (\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n deepPickImpl(record, path) as never\n );\n }\n\n case 1: {\n const [path] = args;\n\n return (record: R) => deepPick(record, path);\n }\n }\n }\n\n /**\n * Deeply omits a nested property from an object along the specified key path.\n * Supports both direct and curried usage.\n *\n * @example\n *\n * ```ts\n * const data = { a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 } as const;\n *\n * // Direct usage\n * const result = Obj.deepOmit(data, ['a', 'b', 'c']);\n *\n * assert.deepStrictEqual(result, { a: { b: { d: 2 }, e: 3 }, f: 4 });\n *\n * // Curried usage with pipe\n * const omitAB = Obj.deepOmit(['a', 'b']);\n *\n * const result2 = pipe(data).map(omitAB).value;\n *\n * assert.deepStrictEqual(result2, { a: { e: 3 }, f: 4 });\n * ```\n *\n * @template R - The type of the input record\n * @template Path - The key path tuple\n * @param record - The source record\n * @param path - A readonly tuple of keys representing the nested path to omit\n * @returns A new record with the nested property at the path removed\n */\n export function deepOmit<\n const R extends UnknownRecord,\n const Path extends readonly (string | number)[],\n >(record: R, path: Path): DeepOmit<R, Path>;\n\n // Curried version\n export function deepOmit<const Path extends readonly (string | number)[]>(\n path: Path,\n ): <const R extends UnknownRecord>(record: R) => DeepOmit<R, Path>;\n\n export function deepOmit<\n const R extends UnknownRecord,\n const Path extends readonly (string | number)[],\n >(\n ...args: readonly [record: R, path: Path] | readonly [path: Path]\n ): DeepOmit<R, Path> | ((record: R) => DeepOmit<R, Path>) {\n switch (args.length) {\n case 2: {\n const [record, path] = args;\n\n return (\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n deepOmitImpl(record, path) as never\n );\n }\n\n case 1: {\n const [path] = args;\n\n return (record: R) => deepOmit(record, path);\n }\n }\n }\n}\n\nconst deepPickImpl = (\n record: UnknownRecord,\n path: readonly (string | number)[],\n): UnknownRecord => {\n if (!Arr.isNonEmpty(path)) return record;\n\n const head = path[0];\n\n if (!hasKey(record, head)) return {};\n\n const value = record[head];\n\n const tail = path.slice(1);\n\n if (!Arr.isNonEmpty(tail)) return { [head]: value };\n\n if (!isRecord(value)) return { [head]: {} };\n\n return {\n [head]: deepPickImpl(value, tail),\n };\n};\n\nconst deepOmitImpl = (\n record: UnknownRecord,\n path: readonly (string | number)[],\n): UnknownRecord => {\n if (!Arr.isNonEmpty(path)) return record;\n\n const head = path[0];\n\n if (!hasKey(record, head)) return record;\n\n const tail = path.slice(1);\n\n if (!Arr.isNonEmpty(tail)) {\n return Object.fromEntries(\n Object.entries(record).filter(([k]) => k !== head),\n );\n }\n\n const value = record[head];\n\n if (!isRecord(value)) return record;\n\n return {\n ...record,\n [head]: deepOmitImpl(value, tail),\n };\n};\n\n/**\n * @internal\n * Internal type utilities for the Obj module.\n */\ndeclare namespace TsDataForgeInternals {\n type RecursionLimit = 20;\n\n /** - `[['x', 1], ['y', 3]]` -> `{ x: 1, y: 3 }` */\n export type EntriesToObject<\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n > = Readonly<EntriesToObjectImpl<{}, Entries>>;\n\n /** @internal */\n type EntriesToObjectImpl<\n R,\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n > =\n TypeEq<Entries['length'], 0> extends true\n ? R\n : EntriesToObjectImpl<\n R & ReadonlyRecord<Entries[0][0], Entries[0][1]>,\n List.Tail<Entries>\n >;\n\n /**\n * - `['x' | 'y' | 'z', number][]]` -> `'x' | 'y' | 'z'`\n * - `[['a' | 'b' | 'c', number], ...['x' | 'y' | 'z', number][]]` -> `'a' |\n * 'b' | 'c' | 'x' | 'y' | 'z'`\n *\n * @note To handle the second example above, recursion needs to be performed on infinite-length Entries,\n * but since the timing to stop cannot be determined, a recursion limit is set.\n */\n export type KeysOfEntries<\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n > = KeysOfEntriesImpl<never, Entries, RecursionLimit>;\n\n type KeysOfEntriesImpl<\n K,\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n RemainingNumRecursions extends number,\n > =\n TypeEq<RemainingNumRecursions, 0> extends true\n ? K\n : TypeEq<Entries['length'], 0> extends true\n ? K\n : KeysOfEntriesImpl<\n K | Entries[0][0],\n List.Tail<Entries>,\n Decrement<RemainingNumRecursions>\n >;\n\n export type ValuesOfEntries<\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n > = ValuesOfEntriesImpl<never, Entries, RecursionLimit>;\n\n type ValuesOfEntriesImpl<\n K,\n Entries extends readonly (readonly [PropertyKey, unknown])[],\n RemainingNumRecursions extends number,\n > =\n TypeEq<RemainingNumRecursions, 0> extends true\n ? K\n : TypeEq<Entries['length'], 0> extends true\n ? K\n : ValuesOfEntriesImpl<\n K | Entries[0][1],\n List.Tail<Entries>,\n Decrement<RemainingNumRecursions>\n >;\n\n export type PartialIfKeyIsUnion<K, T> =\n IsUnion<K> extends true ? Partial<T> : T;\n\n /**\n * The keys `Object.entries` yields for `R`: symbol keys are not enumerated,\n * and a numeric key `1` arrives as the string `'1'`.\n */\n export type MappedRecordKey<R> = ToString<RelaxedExclude<keyof R, symbol>>;\n\n /**\n * `R` with every value replaced by `V2`. Homomorphic over `keyof R`, so\n * optional modifiers and index signatures are preserved; the key remapping\n * drops the symbol keys that `Object.entries` would not have visited.\n */\n export type MappedRecord<R, V2> = Readonly<{\n [K in keyof R as K extends symbol ? never : K]: V2;\n }>;\n\n /** `R` without the symbol keys `Object.entries` never visits. */\n type StringKeyedRecord<R> = Readonly<{\n [K in keyof R as K extends symbol ? never : K]: R[K];\n }>;\n\n /**\n * The result of dropping entries from `R`: symbol keys go away, and the\n * remaining keys become optional because a predicate may reject any of them.\n *\n * A record keyed by an index signature stays total instead. It names no\n * specific key that could go missing, and `noUncheckedIndexedAccess` already\n * adds `| undefined` on access, so making it `Partial` would add nothing and\n * would only stop the result from being assignable back to the record type it\n * came from.\n */\n export type FilteredRecord<R> = string extends keyof R\n ? StringKeyedRecord<R>\n : number extends keyof R\n ? StringKeyedRecord<R>\n : Readonly<{ [K in keyof R as K extends symbol ? never : K]?: R[K] }>;\n\n /**\n * Merges two object types where keys in B override keys in A, preserving optional modifiers.\n *\n * This implementation uses a sophisticated technique to preserve optional property modifiers (`?`)\n * during the merge operation, which would otherwise be lost in a naive mapped type implementation.\n *\n * ## Core Technique: Optional Property Detection\n *\n * Uses `{} extends Pick<T, K>` to determine if a property is optional:\n * - **Required property**: `Pick<T, 'x'>` = `{ x: number }` → `{}` does NOT extend it (false)\n * - **Optional property**: `Pick<T, 'y'>` = `{ y?: string }` → `{}` DOES extend it (true)\n *\n * This works because `{}` (empty object) is assignable to objects with only optional properties\n * but not to objects with required properties.\n *\n * ## Implementation Strategy\n *\n * The type is constructed as an intersection of five mapped types:\n *\n * 1. **Required properties from B** - Properties in B that are required (override A completely)\n * 2. **Optional properties from B (not in A)** - New optional properties from B\n * 3. **Optional properties from B (in A)** - Union of A[K] | B[K] to preserve both possibilities\n * 4. **Required properties only in A** - Properties in A but not in B that are required\n * 5. **Optional properties only in A** - Properties in A but not in B that are optional (with `?` modifier)\n *\n * Finally, the intersection is flattened using an optional-preserving identity mapping that\n * re-applies the optional detection logic to maintain the `?` modifiers in the final type.\n *\n * @example\n * ```ts\n * type A = Readonly<{ x: number; y?: string }>;\n *\n * type B = Readonly<{ y?: number; z?: boolean }>;\n *\n * type Result = MergeTwo<A, B>;\n * // Result: Readonly<{ x: number; y?: string | number; z?: boolean }>\n * ```\n */\n // transformer-ignore-next-line\n type MergeTwo<A extends UnknownRecord, B extends UnknownRecord> = {\n // 1. Required properties from B (override A completely)\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n readonly [K in keyof B as {} extends StrictPick<B, K> ? never : K]: B[K];\n } & {\n // 2. Optional properties from B that are NOT in A\n readonly [\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n K in keyof B as {} extends StrictPick<B, K>\n ? K extends keyof A\n ? never\n : K\n : never\n ]?: B[K];\n } & {\n // 3. Optional properties from B that ARE in A (create union)\n readonly [\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n K in keyof B as {} extends StrictPick<B, K>\n ? K extends keyof A\n ? K\n : never\n : never\n ]?: K extends keyof A ? A[K] | B[K] : never;\n } & {\n // 4. Required properties only in A (not overridden by B)\n readonly [\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n K in RelaxedExclude<keyof A, keyof B> as {} extends StrictPick<A, K>\n ? never\n : K\n ]: A[K];\n } & {\n // 5. Optional properties only in A (not overridden by B)\n readonly [\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n K in RelaxedExclude<keyof A, keyof B> as {} extends StrictPick<A, K>\n ? K\n : never\n ]?: A[K];\n } extends infer O\n ? Readonly<\n {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n [K in keyof O as {} extends StrictPick<O, K> ? never : K]: O[K];\n } & {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n [K in keyof O as {} extends StrictPick<O, K> ? K : never]?: O[K];\n }\n >\n : never;\n\n /** Sequentially merges a tuple of object types from left to right. */\n export type MergeAll<\n Records extends readonly UnknownRecord[],\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n Acc extends UnknownRecord = {},\n > =\n IsFixedLengthList<Records> extends false\n ? ArrayElement<Records>\n : Records extends readonly [\n infer First extends UnknownRecord,\n ...infer Rest extends readonly UnknownRecord[],\n ]\n ? MergeAll<Rest, MergeTwo<Acc, First>>\n : Acc;\n}\n"],"names":["Obj","Optional.isSome","Arr.isNonEmpty"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-data-forge",
3
- "version": "14.2.1",
3
+ "version": "14.4.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript",
@@ -42,26 +42,24 @@
42
42
  "devDependencies": {
43
43
  "@emotion/react": "11.14.0",
44
44
  "@emotion/styled": "11.14.1",
45
- "@mui/material": "9.2.0",
46
- "@rollup/plugin-replace": "6.0.3",
47
- "@rollup/plugin-strip": "3.0.4",
45
+ "@mui/material": "9.3.1",
48
46
  "@types/react": "19.2.18",
49
47
  "@vitest/browser-playwright": "4.1.10",
50
48
  "dedent": "1.7.2",
51
- "immer": "11.1.15",
49
+ "immer": "11.1.16",
50
+ "playwright": "1.62.1",
52
51
  "prettier": "3.9.6",
53
52
  "prettier-plugin-organize-imports": "4.3.0",
54
53
  "prettier-plugin-packagejson": "3.0.2",
55
54
  "react": "19.2.8",
56
55
  "rollup": "4.62.4",
57
- "rollup-plugin-esbuild": "6.2.1",
58
56
  "typedoc": "0.28.20",
59
57
  "typedoc-github-theme": "0.4.0",
60
- "vite": "8.2.0",
61
- "eslint-config-typed": "5.8.5",
62
- "eslint-plugin-ts-fortress": "1.0.1",
63
- "eslint-plugin-ts-type-forge": "0.6.3",
64
- "ts-repo-utils": "10.2.0"
58
+ "vite": "8.2.1",
59
+ "eslint-plugin-ts-fortress": "1.0.4",
60
+ "eslint-config-typed": "5.9.3",
61
+ "ts-repo-utils": "10.2.0",
62
+ "eslint-plugin-ts-type-forge": "0.6.6"
65
63
  },
66
64
  "peerDependencies": {
67
65
  "typescript": ">=4.8"
@@ -51,7 +51,6 @@ export type Stack<T> = Readonly<{
51
51
  *
52
52
  * stack.push('first');
53
53
  *
54
- * // eslint-disable-next-line unicorn/prefer-single-call
55
54
  * stack.push('second');
56
55
  *
57
56
  * assert.isFalse(stack.isEmpty);
@@ -87,7 +86,6 @@ export type Stack<T> = Readonly<{
87
86
  *
88
87
  * stack.push('first');
89
88
  *
90
- * // eslint-disable-next-line unicorn/prefer-single-call
91
89
  * stack.push('second');
92
90
  *
93
91
  * assert.isFalse(stack.isEmpty);
@@ -123,7 +121,6 @@ export type Stack<T> = Readonly<{
123
121
  *
124
122
  * stack.push('first');
125
123
  *
126
- * // eslint-disable-next-line unicorn/prefer-single-call
127
124
  * stack.push('second');
128
125
  *
129
126
  * assert.isFalse(stack.isEmpty);
@@ -162,7 +159,6 @@ export type Stack<T> = Readonly<{
162
159
  *
163
160
  * stack.push('first');
164
161
  *
165
- * // eslint-disable-next-line unicorn/prefer-single-call
166
162
  * stack.push('second');
167
163
  *
168
164
  * assert.isFalse(stack.isEmpty);
@@ -363,7 +359,6 @@ class StackClass<T> implements Stack<T> {
363
359
  *
364
360
  * stack.push('first');
365
361
  *
366
- * // eslint-disable-next-line unicorn/prefer-single-call
367
362
  * stack.push('second');
368
363
  *
369
364
  * assert.isFalse(stack.isEmpty);
@@ -59,10 +59,8 @@ describe('Stack test', () => {
59
59
 
60
60
  stack.push('first');
61
61
 
62
- // eslint-disable-next-line unicorn/prefer-single-call
63
62
  stack.push('second');
64
63
 
65
- // eslint-disable-next-line unicorn/prefer-single-call
66
64
  stack.push('third');
67
65
 
68
66
  expect(stack.size).toBe(3);
@@ -130,14 +128,12 @@ describe('Stack test', () => {
130
128
 
131
129
  stack.push('a');
132
130
 
133
- // eslint-disable-next-line unicorn/prefer-single-call
134
131
  stack.push('b');
135
132
 
136
133
  expect(Optional.unwrap(stack.pop())).toBe('b');
137
134
 
138
135
  stack.push('c');
139
136
 
140
- // eslint-disable-next-line unicorn/prefer-single-call
141
137
  stack.push('d');
142
138
 
143
139
  expect(Optional.unwrap(stack.pop())).toBe('d');
@@ -160,7 +156,6 @@ describe('Stack test', () => {
160
156
 
161
157
  stack.push(item1);
162
158
 
163
- // eslint-disable-next-line unicorn/prefer-single-call
164
159
  stack.push(item2);
165
160
 
166
161
  assert.deepStrictEqual(Optional.unwrap(stack.pop()), item2);
@@ -221,13 +216,10 @@ describe('Stack test', () => {
221
216
 
222
217
  stack.push('value');
223
218
 
224
- // eslint-disable-next-line unicorn/prefer-single-call
225
219
  stack.push(null);
226
220
 
227
- // eslint-disable-next-line unicorn/prefer-single-call
228
221
  stack.push(undefined);
229
222
 
230
- // eslint-disable-next-line unicorn/prefer-single-call
231
223
  stack.push('another');
232
224
 
233
225
  expect(Optional.unwrap(stack.pop())).toBe('another');
@@ -12,10 +12,12 @@ import {
12
12
  type RelaxedPick,
13
13
  type StrictOmit,
14
14
  type StrictPick,
15
+ type ToString,
15
16
  type TypeEq,
16
17
  type UnknownRecord,
17
18
  } from 'ts-type-forge';
18
19
  import { Arr } from '../array/index.mjs';
20
+ import { Optional } from '../functional/index.mjs';
19
21
  import { hasKey, isRecord } from '../guard/index.mjs';
20
22
 
21
23
  /**
@@ -326,6 +328,241 @@ export namespace Obj {
326
328
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
327
329
  Object.fromEntries(entries) as never;
328
330
 
331
+ /**
332
+ * Transforms every value of a record, leaving the key set untouched.
333
+ *
334
+ * **Type Behavior**: the result is a mapped type over `keyof R`, so the keys
335
+ * — and their optional modifiers — survive verbatim:
336
+ * `{ a: number; b?: number }` maps to `{ a: V2; b?: V2 }`, and a record with
337
+ * an index signature stays total rather than becoming `Partial`.
338
+ *
339
+ * This is what `Object.fromEntries(Object.entries(record).map(...))` cannot
340
+ * express: an entries array carries no evidence that it still covers every
341
+ * key of the record it came from, so its element type is all the return type
342
+ * can be built from. Going through a mapped type instead states the
343
+ * invariant that a value-only transform preserves the key set.
344
+ *
345
+ * **Key handling**: keys reach the callback the way `Object.entries` yields
346
+ * them, so a numeric key `1` arrives as `'1'`. Symbol-keyed properties are
347
+ * not enumerated by `Object.entries`; they are dropped from both the result
348
+ * and its type.
349
+ *
350
+ * @example
351
+ *
352
+ * <!-- doc:embed:jsdoc:example:./samples/src/object/map-example.mts -->
353
+ *
354
+ * ```ts
355
+ * const scores = { alice: 1, bob: 2 } as const;
356
+ *
357
+ * const doubled = Obj.map(scores, (value) => value * 2);
358
+ *
359
+ * assert.deepStrictEqual(doubled, { alice: 2, bob: 4 });
360
+ *
361
+ * // The key is available as the second argument.
362
+ * const labelled = Obj.map(scores, (value, key) => `${key}=${value}`);
363
+ *
364
+ * assert.deepStrictEqual(labelled, { alice: 'alice=1', bob: 'bob=2' });
365
+ * ```
366
+ *
367
+ * <!-- /doc:embed:jsdoc:example:./samples/src/object/map-example.mts -->
368
+ *
369
+ * @template R - The type of the source record
370
+ * @template V2 - The type the callback returns for every key
371
+ * @param record - The record whose values are transformed
372
+ * @param mapFn - Called once per own enumerable string key, with the value
373
+ * and the key
374
+ * @returns A new record with the same keys as `record` and the mapped values
375
+ */
376
+ export const map = <const R extends UnknownRecord, V2>(
377
+ record: R,
378
+ mapFn: (
379
+ value: R[keyof R],
380
+ key: TsDataForgeInternals.MappedRecordKey<R>,
381
+ ) => V2,
382
+ ): TsDataForgeInternals.MappedRecord<R, V2> => {
383
+ // `Object.entries` erases both the key and the value type of `R`, so the
384
+ // callback is widened once here instead of asserting on every entry.
385
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
386
+ const mapFnWidened = mapFn as (value: unknown, key: string) => V2;
387
+
388
+ return (
389
+ // `Object.entries` enumerates exactly the own enumerable string keys of
390
+ // `record`, and nothing here adds or removes an entry, so the result has
391
+ // the key set `MappedRecord` claims.
392
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
393
+ Object.fromEntries(
394
+ Object.entries(record).map(
395
+ ([k, v]) => [k, mapFnWidened(v, k)] as const,
396
+ ),
397
+ ) as never
398
+ );
399
+ };
400
+
401
+ /**
402
+ * Keeps the entries whose value the predicate accepts, leaving the values
403
+ * themselves untouched.
404
+ *
405
+ * **Type Behavior**: the surviving keys are the keys of the source record,
406
+ * made optional — the predicate may reject any of them. A record keyed by an
407
+ * index signature stays total instead, because it names no specific key that
408
+ * could go missing.
409
+ *
410
+ * Passing a type guard narrows the value type of the result, which is what
411
+ * `Object.fromEntries(Object.entries(record).filter(guard))` loses.
412
+ *
413
+ * **Key handling**: keys reach the callback the way `Object.entries` yields
414
+ * them, so a numeric key `1` arrives as `'1'`. Symbol-keyed properties are
415
+ * not enumerated by `Object.entries`; they are dropped from both the result
416
+ * and its type.
417
+ *
418
+ * @example
419
+ *
420
+ * <!-- doc:embed:jsdoc:example:./samples/src/object/filter-example.mts -->
421
+ *
422
+ * ```ts
423
+ * const scores = { alice: 1, bob: 2 } as const;
424
+ *
425
+ * const passing = Obj.filter(scores, (value) => value > 1);
426
+ *
427
+ * assert.deepStrictEqual(passing, { bob: 2 });
428
+ *
429
+ * // The key is available as the second argument.
430
+ * const notAlice = Obj.filter(scores, (_value, key) => key !== 'alice');
431
+ *
432
+ * assert.deepStrictEqual(notAlice, { bob: 2 });
433
+ *
434
+ * // A type guard narrows the value type of the result.
435
+ * const mixed: UnknownRecord = { a: 1, b: 'x' } as const;
436
+ *
437
+ * const strings = Obj.filter(mixed, isString);
438
+ *
439
+ * assert.deepStrictEqual(strings, { b: 'x' });
440
+ * ```
441
+ *
442
+ * <!-- /doc:embed:jsdoc:example:./samples/src/object/filter-example.mts -->
443
+ *
444
+ * @template R - The type of the source record
445
+ * @template V2 - The type the guard narrows values to
446
+ * @param record - The record whose entries are filtered
447
+ * @param predicate - Called once per own enumerable string key, with the
448
+ * value and the key
449
+ * @returns A new record holding only the accepted entries
450
+ */
451
+ export function filter<const R extends UnknownRecord, V2 extends R[keyof R]>(
452
+ record: R,
453
+ predicate: (
454
+ value: R[keyof R],
455
+ key: TsDataForgeInternals.MappedRecordKey<R>,
456
+ ) => value is V2,
457
+ ): TsDataForgeInternals.FilteredRecord<
458
+ TsDataForgeInternals.MappedRecord<R, V2>
459
+ >;
460
+
461
+ export function filter<const R extends UnknownRecord>(
462
+ record: R,
463
+ predicate: (
464
+ value: R[keyof R],
465
+ key: TsDataForgeInternals.MappedRecordKey<R>,
466
+ ) => boolean,
467
+ ): TsDataForgeInternals.FilteredRecord<R>;
468
+
469
+ export function filter<const R extends UnknownRecord>(
470
+ record: R,
471
+ predicate: (
472
+ value: R[keyof R],
473
+ key: TsDataForgeInternals.MappedRecordKey<R>,
474
+ ) => boolean,
475
+ ): TsDataForgeInternals.FilteredRecord<R> {
476
+ // `Object.entries` erases both the key and the value type of `R`, so the
477
+ // callback is widened once here instead of asserting on every entry.
478
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
479
+ const predicateWidened = predicate as (
480
+ value: unknown,
481
+ key: string,
482
+ ) => boolean;
483
+
484
+ return (
485
+ // `Object.entries` enumerates exactly the own enumerable string keys of
486
+ // `record`, and filtering only ever removes entries, so the result has a
487
+ // subset of those keys — which is what `FilteredRecord` claims.
488
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
489
+ Object.fromEntries(
490
+ Object.entries(record).filter(([k, v]) => predicateWidened(v, k)),
491
+ ) as never
492
+ );
493
+ }
494
+
495
+ /**
496
+ * Transforms and drops entries in one pass: the callback returns
497
+ * `Optional.some(value)` to keep an entry with a new value, or
498
+ * `Optional.none` to drop it.
499
+ *
500
+ * **Type Behavior**: as with {@link filter}, the surviving keys are the keys
501
+ * of the source record made optional, and a record keyed by an index
502
+ * signature stays total. Unlike {@link filter}, every kept value takes the
503
+ * type the callback returns.
504
+ *
505
+ * `Optional` rather than `undefined` marks the dropped entries, so a record
506
+ * whose values are legitimately `undefined` stays expressible.
507
+ *
508
+ * **Key handling**: keys reach the callback the way `Object.entries` yields
509
+ * them, so a numeric key `1` arrives as `'1'`. Symbol-keyed properties are
510
+ * not enumerated by `Object.entries`; they are dropped from both the result
511
+ * and its type.
512
+ *
513
+ * @example
514
+ *
515
+ * <!-- doc:embed:jsdoc:example:./samples/src/object/filter-map-example.mts -->
516
+ *
517
+ * ```ts
518
+ * const scores = { alice: 1, bob: 2 } as const;
519
+ *
520
+ * // `Optional.some` keeps an entry with a new value, `Optional.none` drops it.
521
+ * const labelled = Obj.filterMap(scores, (value, key) =>
522
+ * value > 1 ? Optional.some(`${key}=${value}`) : Optional.none,
523
+ * );
524
+ *
525
+ * assert.deepStrictEqual(labelled, { bob: 'bob=2' });
526
+ * ```
527
+ *
528
+ * <!-- /doc:embed:jsdoc:example:./samples/src/object/filter-map-example.mts -->
529
+ *
530
+ * @template R - The type of the source record
531
+ * @template V2 - The type the callback returns for the entries it keeps
532
+ * @param record - The record whose entries are transformed and filtered
533
+ * @param mapFn - Called once per own enumerable string key, with the value
534
+ * and the key
535
+ * @returns A new record holding only the kept entries, with mapped values
536
+ */
537
+ export const filterMap = <const R extends UnknownRecord, V2>(
538
+ record: R,
539
+ mapFn: (
540
+ value: R[keyof R],
541
+ key: TsDataForgeInternals.MappedRecordKey<R>,
542
+ ) => Optional<V2>,
543
+ ): TsDataForgeInternals.FilteredRecord<
544
+ TsDataForgeInternals.MappedRecord<R, V2>
545
+ > => {
546
+ // `Object.entries` erases both the key and the value type of `R`, so the
547
+ // callback is widened once here instead of asserting on every entry.
548
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
549
+ const mapFnWidened = mapFn as (value: unknown, key: string) => Optional<V2>;
550
+
551
+ return (
552
+ // `Object.entries` enumerates exactly the own enumerable string keys of
553
+ // `record`, and this only ever drops entries or replaces their values, so
554
+ // the result has a subset of those keys with `V2` values.
555
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
556
+ Object.fromEntries(
557
+ Object.entries(record).flatMap(([k, v]) => {
558
+ const mapped = mapFnWidened(v, k);
559
+
560
+ return Optional.isSome(mapped) ? [[k, mapped.value] as const] : [];
561
+ }),
562
+ ) as never
563
+ );
564
+ };
565
+
329
566
  /**
330
567
  * Merges multiple records into a single record using `Object.assign`.
331
568
  * Later records override properties from earlier records with the same key.
@@ -605,6 +842,42 @@ declare namespace TsDataForgeInternals {
605
842
  export type PartialIfKeyIsUnion<K, T> =
606
843
  IsUnion<K> extends true ? Partial<T> : T;
607
844
 
845
+ /**
846
+ * The keys `Object.entries` yields for `R`: symbol keys are not enumerated,
847
+ * and a numeric key `1` arrives as the string `'1'`.
848
+ */
849
+ export type MappedRecordKey<R> = ToString<RelaxedExclude<keyof R, symbol>>;
850
+
851
+ /**
852
+ * `R` with every value replaced by `V2`. Homomorphic over `keyof R`, so
853
+ * optional modifiers and index signatures are preserved; the key remapping
854
+ * drops the symbol keys that `Object.entries` would not have visited.
855
+ */
856
+ export type MappedRecord<R, V2> = Readonly<{
857
+ [K in keyof R as K extends symbol ? never : K]: V2;
858
+ }>;
859
+
860
+ /** `R` without the symbol keys `Object.entries` never visits. */
861
+ type StringKeyedRecord<R> = Readonly<{
862
+ [K in keyof R as K extends symbol ? never : K]: R[K];
863
+ }>;
864
+
865
+ /**
866
+ * The result of dropping entries from `R`: symbol keys go away, and the
867
+ * remaining keys become optional because a predicate may reject any of them.
868
+ *
869
+ * A record keyed by an index signature stays total instead. It names no
870
+ * specific key that could go missing, and `noUncheckedIndexedAccess` already
871
+ * adds `| undefined` on access, so making it `Partial` would add nothing and
872
+ * would only stop the result from being assignable back to the record type it
873
+ * came from.
874
+ */
875
+ export type FilteredRecord<R> = string extends keyof R
876
+ ? StringKeyedRecord<R>
877
+ : number extends keyof R
878
+ ? StringKeyedRecord<R>
879
+ : Readonly<{ [K in keyof R as K extends symbol ? never : K]?: R[K] }>;
880
+
608
881
  /**
609
882
  * Merges two object types where keys in B override keys in A, preserving optional modifiers.
610
883
  *
@@ -5,7 +5,8 @@ import {
5
5
  type UnknownRecord,
6
6
  } from 'ts-type-forge';
7
7
  import { expectType } from '../expect-type.mjs';
8
- import { pipe } from '../functional/index.mjs';
8
+ import { Optional, pipe } from '../functional/index.mjs';
9
+ import { isString } from '../guard/index.mjs';
9
10
  import { Obj } from './object.mjs';
10
11
 
11
12
  describe('shallowEq', () => {
@@ -188,6 +189,221 @@ describe('fromEntries', () => {
188
189
  });
189
190
  });
190
191
 
192
+ describe('map', () => {
193
+ test('should map values while keeping the key set', () => {
194
+ const result = Obj.map({ a: 1, b: 2 }, (value) => value * 2);
195
+
196
+ expectType<typeof result, Readonly<{ a: number; b: number }>>('=');
197
+
198
+ assert.deepStrictEqual(result, { a: 2, b: 4 });
199
+ });
200
+
201
+ test('should keep a record with an index signature total', () => {
202
+ const scores: ReadonlyRecord<string, number> = {
203
+ alice: 1,
204
+ bob: 2,
205
+ } as const;
206
+
207
+ const result = Obj.map(scores, (value) => value > 1);
208
+
209
+ expectType<typeof result, ReadonlyRecord<string, boolean>>('=');
210
+
211
+ assert.deepStrictEqual(result, { alice: false, bob: true });
212
+ });
213
+
214
+ test('should preserve optional property modifiers', () => {
215
+ const config: Readonly<{ host: string; port?: number }> = {
216
+ host: 'a',
217
+ } as const;
218
+
219
+ const result = Obj.map(config, (value) => value === undefined);
220
+
221
+ expectType<typeof result, Readonly<{ host: boolean; port?: boolean }>>('=');
222
+
223
+ assert.deepStrictEqual(result, { host: false });
224
+ });
225
+
226
+ test('should pass the key as the second argument', () => {
227
+ const result = Obj.map({ a: 1, b: 2 }, (value, key) => {
228
+ expectType<typeof key, 'a' | 'b'>('=');
229
+
230
+ return `${key}${value}`;
231
+ });
232
+
233
+ assert.deepStrictEqual(result, { a: 'a1', b: 'b2' });
234
+ });
235
+
236
+ test('should widen the value to the union of the record values', () => {
237
+ const result = Obj.map({ a: 1, b: 'x' }, (value) => {
238
+ expectType<typeof value, 1 | 'x'>('=');
239
+
240
+ return value;
241
+ });
242
+
243
+ expectType<typeof result, Readonly<{ a: 1 | 'x'; b: 1 | 'x' }>>('=');
244
+
245
+ assert.deepStrictEqual(result, { a: 1, b: 'x' });
246
+ });
247
+
248
+ test('should stringify numeric keys, as Object.entries does', () => {
249
+ const result = Obj.map({ 1: 'x', 2: 'y' }, (value, key) => {
250
+ expectType<typeof key, '1' | '2'>('=');
251
+
252
+ return `${key}${value}`;
253
+ });
254
+
255
+ expectType<typeof result, Readonly<{ 1: string; 2: string }>>('=');
256
+
257
+ assert.deepStrictEqual(result, { 1: '1x', 2: '2y' });
258
+ });
259
+
260
+ test('should drop symbol keys, as Object.entries does', () => {
261
+ const sym = Symbol('sym');
262
+
263
+ const withSymbol = { [sym]: 1, a: 2 } as const;
264
+
265
+ const result = Obj.map(withSymbol, (value) => value * 2);
266
+
267
+ expectType<typeof result, Readonly<{ a: number }>>('=');
268
+
269
+ assert.deepStrictEqual(result, { a: 4 });
270
+ });
271
+
272
+ test('should handle an empty record', () => {
273
+ const result = Obj.map({}, (value) => value);
274
+
275
+ assert.deepStrictEqual(result, {});
276
+ });
277
+
278
+ test('should not mutate the source record', () => {
279
+ const source = { a: 1, b: 2 } as const;
280
+
281
+ const result = Obj.map(source, (value) => value * 2);
282
+
283
+ assert.deepStrictEqual(source, { a: 1, b: 2 });
284
+
285
+ assert.notStrictEqual<UnknownRecord>(result, source);
286
+ });
287
+ });
288
+
289
+ describe('filter', () => {
290
+ test('should keep the entries the predicate accepts', () => {
291
+ const result = Obj.filter({ a: 1, b: 2 }, (value) => value > 1);
292
+
293
+ expectType<typeof result, Readonly<{ a?: 1; b?: 2 }>>('=');
294
+
295
+ assert.deepStrictEqual(result, { b: 2 });
296
+ });
297
+
298
+ test('should keep a record with an index signature total', () => {
299
+ const scores: ReadonlyRecord<string, number> = {
300
+ alice: 1,
301
+ bob: 2,
302
+ } as const;
303
+
304
+ const result = Obj.filter(scores, (value) => value > 1);
305
+
306
+ expectType<typeof result, ReadonlyRecord<string, number>>('=');
307
+
308
+ assert.deepStrictEqual(result, { bob: 2 });
309
+ });
310
+
311
+ test('should pass the key as the second argument', () => {
312
+ const result = Obj.filter({ a: 1, b: 2 }, (_value, key) => {
313
+ expectType<typeof key, 'a' | 'b'>('=');
314
+
315
+ return key === 'a';
316
+ });
317
+
318
+ assert.deepStrictEqual(result, { a: 1 });
319
+ });
320
+
321
+ test('should narrow the value type through a type guard', () => {
322
+ const mixed: UnknownRecord = { a: 1, b: 'x' } as const;
323
+
324
+ const result = Obj.filter(mixed, isString);
325
+
326
+ expectType<typeof result, ReadonlyRecord<string, string>>('=');
327
+
328
+ assert.deepStrictEqual(result, { b: 'x' });
329
+ });
330
+
331
+ test('should drop symbol keys, as Object.entries does', () => {
332
+ const sym = Symbol('s');
333
+
334
+ const result = Obj.filter({ a: 1, [sym]: 2 }, () => true);
335
+
336
+ expectType<typeof result, Readonly<{ a?: 1 }>>('=');
337
+
338
+ assert.deepStrictEqual(result, { a: 1 });
339
+ });
340
+
341
+ test('should handle an empty record', () => {
342
+ const result = Obj.filter({}, () => true);
343
+
344
+ assert.deepStrictEqual(result, {});
345
+ });
346
+
347
+ test('should not mutate the source record', () => {
348
+ const source = { a: 1, b: 2 } as const;
349
+
350
+ Obj.filter(source, (value) => value > 1);
351
+
352
+ assert.deepStrictEqual(source, { a: 1, b: 2 });
353
+ });
354
+ });
355
+
356
+ describe('filterMap', () => {
357
+ test('should keep Optional.some and drop Optional.none', () => {
358
+ const result = Obj.filterMap({ a: 1, b: 2 }, (value) =>
359
+ value > 1 ? Optional.some(value * 10) : Optional.none,
360
+ );
361
+
362
+ expectType<typeof result, Readonly<{ a?: number; b?: number }>>('=');
363
+
364
+ assert.deepStrictEqual(result, { b: 20 });
365
+ });
366
+
367
+ test('should keep a record with an index signature total', () => {
368
+ const scores: ReadonlyRecord<string, number> = {
369
+ alice: 1,
370
+ bob: 2,
371
+ } as const;
372
+
373
+ const result = Obj.filterMap(scores, (value) =>
374
+ value > 1 ? Optional.some(value * 10) : Optional.none,
375
+ );
376
+
377
+ expectType<typeof result, ReadonlyRecord<string, number>>('=');
378
+
379
+ assert.deepStrictEqual(result, { bob: 20 });
380
+ });
381
+
382
+ test('should pass the key as the second argument', () => {
383
+ const result = Obj.filterMap({ a: 1, b: 2 }, (value, key) => {
384
+ expectType<typeof key, 'a' | 'b'>('=');
385
+
386
+ return key === 'a' ? Optional.some(value) : Optional.none;
387
+ });
388
+
389
+ assert.deepStrictEqual(result, { a: 1 });
390
+ });
391
+
392
+ test('should handle an empty record', () => {
393
+ const result = Obj.filterMap({}, () => Optional.none);
394
+
395
+ assert.deepStrictEqual(result, {});
396
+ });
397
+
398
+ test('should not mutate the source record', () => {
399
+ const source = { a: 1, b: 2 } as const;
400
+
401
+ Obj.filterMap(source, (value) => Optional.some(value * 2));
402
+
403
+ assert.deepStrictEqual(source, { a: 1, b: 2 });
404
+ });
405
+ });
406
+
191
407
  describe('merge', () => {
192
408
  test('should merge two objects, later overriding earlier', () => {
193
409
  const a = { a: 0, b: 0 } as const;