From 134652b8e6e48f71d6a453059700c00d470db1c2 Mon Sep 17 00:00:00 2001 From: Hugo <95517615+HugoHSun@users.noreply.github.com> Date: Tue, 26 May 2026 12:42:46 +1000 Subject: [PATCH 1/3] feat: add array empty object option --- .changeset/empty-object-array-items.md | 5 +++ README.md | 20 ++++++++++ src/index.ts | 7 +++- test/index.test.ts | 51 ++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 .changeset/empty-object-array-items.md diff --git a/.changeset/empty-object-array-items.md b/.changeset/empty-object-array-items.md new file mode 100644 index 0000000..1dc5b33 --- /dev/null +++ b/.changeset/empty-object-array-items.md @@ -0,0 +1,5 @@ +--- +'remove-undefined-objects': minor +--- + +Add `preserveEmptyObjectsInArrays` to control whether empty object array items are preserved separately from empty object properties. diff --git a/README.md b/README.md index 50026bb..e2afa4e 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,26 @@ console.log( // { key1: {}, key2: {}, key3: 123 } ``` +### `preserveEmptyObjectsInArrays` + +Optional boolean. +Controls whether empty object items inside arrays are removed. If omitted, this follows `preserveEmptyObject`. + +```js +import removeUndefinedObjects from 'remove-undefined-objects'; + +console.log(removeUndefinedObjects({ key1: [{}, { nested: undefined }, { key: 'value' }] })); +// { key1: [{ key: 'value' }] } + +console.log( + removeUndefinedObjects( + { key1: [{}, { nested: undefined }, { key: 'value' }], key2: {} }, + { preserveEmptyObject: true, preserveEmptyObjectsInArrays: false }, + ), +); +// { key1: [{ key: 'value' }], key2: {} } +``` + ### `preserveNullishArrays` Optional boolean. diff --git a/src/index.ts b/src/index.ts index b7ad7e5..6e3becc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,10 +13,15 @@ function isEmptyArray(arr: unknown) { interface RemovalOptions { preserveEmptyArray?: boolean; preserveEmptyObject?: boolean; + preserveEmptyObjectsInArrays?: boolean; preserveNullishArrays?: boolean; removeAllFalsy?: boolean; } +function shouldPreserveEmptyObjectInArray(options: RemovalOptions) { + return options.preserveEmptyObjectsInArrays ?? options.preserveEmptyObject; +} + // Remove objects that has undefined value or recursively contain undefined values function removeUndefined(obj: any): any { if (obj === undefined) { @@ -89,7 +94,7 @@ function stripEmptyObjects(obj: any, options: RemovalOptions = {}) { if (typeof value === 'object' && value !== null) { value = stripEmptyObjects(value, options); - if (isEmptyObject(value) && !options.preserveEmptyObject) { + if (isEmptyObject(value) && !shouldPreserveEmptyObjectInArray(options)) { delete cleanObj[idx]; } else if (isEmptyArray(value) && !options.preserveEmptyArray) { delete cleanObj[idx]; diff --git a/test/index.test.ts b/test/index.test.ts index 1fb0d1b..a524c16 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -181,6 +181,57 @@ it('should not remove empty object values from arrays when preserveEmptyObject i }); }); +it('should remove empty object values from arrays when preserveEmptyObjectsInArrays is false', () => { + expect( + removeUndefinedObjects([{}, { a: undefined }, { b: 'b' }], { + preserveEmptyObject: true, + preserveEmptyObjectsInArrays: false, + }), + ).toStrictEqual([{ b: 'b' }]); + + expect( + removeUndefinedObjects( + { + topLevelEmptyObject: {}, + nested: { + emptyObject: {}, + }, + array: [{}, { emptyObject: {} }, { value: 'value' }], + }, + { + preserveEmptyObject: true, + preserveEmptyObjectsInArrays: false, + }, + ), + ).toStrictEqual({ + topLevelEmptyObject: {}, + nested: { + emptyObject: {}, + }, + array: [{ emptyObject: {} }, { value: 'value' }], + }); + + expect( + removeUndefinedObjects( + { value: [{}, { a: undefined }] }, + { + preserveEmptyArray: true, + preserveEmptyObject: true, + preserveEmptyObjectsInArrays: false, + }, + ), + ).toStrictEqual({ value: [] }); +}); + +it('should preserve empty object values from arrays when preserveEmptyObjectsInArrays is true', () => { + expect( + removeUndefinedObjects([{}, { a: undefined }, { b: 'b' }], { + preserveEmptyObject: false, + preserveEmptyObjectsInArrays: true, + }), + ).toStrictEqual([{}, {}, { b: 'b' }]); +}); + it('should not remove null values from arrays when preserveArrayNulls is true', () => { expect(removeUndefinedObjects([null], { preserveNullishArrays: true })).toStrictEqual([null]); expect(removeUndefinedObjects([undefined], { preserveNullishArrays: true })).toBeUndefined(); From d8c59f311cf35659e32c081d63efeb6fe072e676 Mon Sep 17 00:00:00 2001 From: Hugo <95517615+HugoHSun@users.noreply.github.com> Date: Tue, 26 May 2026 17:13:57 +1000 Subject: [PATCH 2/3] feat: support context-aware empty value preservation --- .changeset/empty-object-array-items.md | 2 +- README.md | 51 ++++++++++---- src/index.ts | 29 +++++--- test/index.test.ts | 96 ++++++++++++++++++++++---- 4 files changed, 140 insertions(+), 38 deletions(-) diff --git a/.changeset/empty-object-array-items.md b/.changeset/empty-object-array-items.md index 1dc5b33..45eb112 100644 --- a/.changeset/empty-object-array-items.md +++ b/.changeset/empty-object-array-items.md @@ -2,4 +2,4 @@ 'remove-undefined-objects': minor --- -Add `preserveEmptyObjectsInArrays` to control whether empty object array items are preserved separately from empty object properties. +Allow `preserveEmptyObject` and `preserveEmptyArray` to preserve empty values by context. diff --git a/README.md b/README.md index e2afa4e..20d0f10 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ The following items will NOT be removed: ### `preserveEmptyArray` -Optional boolean. -If provided, empty arrays `[]` will not get removed +Optional boolean or context map. +Controls whether empty arrays will not get removed. ```js import removeUndefinedObjects from 'remove-undefined-objects'; @@ -54,10 +54,28 @@ console.log( // { key1: [], key2: [], nested: { key3: 'a', key4: [] } } ``` +To preserve empty arrays only in certain locations, provide a context map: + +```js +console.log(removeUndefinedObjects([], { preserveEmptyArray: { root: true } })); +// [] + +console.log( + removeUndefinedObjects( + { key1: [], key2: [undefined], key3: [[], ['value']] }, + { preserveEmptyArray: { objectProperty: true } }, + ), +); +// { key1: [], key2: [], key3: [['value']] } + +console.log(removeUndefinedObjects([[], [undefined], ['value']], { preserveEmptyArray: { arrayItem: true } })); +// [[], [], ['value']] +``` + ### `preserveEmptyObject` -Optional boolean. -If provided, empty objects will not be removed. +Optional boolean or context map. +Controls whether empty objects will not be removed. ```js import removeUndefinedObjects from 'remove-undefined-objects'; @@ -71,26 +89,35 @@ console.log( // { key1: {}, key2: {}, key3: 123 } ``` -### `preserveEmptyObjectsInArrays` - -Optional boolean. -Controls whether empty object items inside arrays are removed. If omitted, this follows `preserveEmptyObject`. +To preserve empty objects only in certain locations, provide a context map: ```js -import removeUndefinedObjects from 'remove-undefined-objects'; +console.log(removeUndefinedObjects({}, { preserveEmptyObject: { root: true } })); +// {} console.log(removeUndefinedObjects({ key1: [{}, { nested: undefined }, { key: 'value' }] })); // { key1: [{ key: 'value' }] } console.log( removeUndefinedObjects( - { key1: [{}, { nested: undefined }, { key: 'value' }], key2: {} }, - { preserveEmptyObject: true, preserveEmptyObjectsInArrays: false }, + { key1: [{}, { nested: undefined }, { key: 'value' }], key2: {}, key3: { nested: undefined } }, + { preserveEmptyObject: { objectProperty: true } }, ), ); -// { key1: [{ key: 'value' }], key2: {} } +// { key1: [{ key: 'value' }], key2: {}, key3: {} } + +console.log( + removeUndefinedObjects([{}, { nested: undefined }, { key: 'value' }], { preserveEmptyObject: { arrayItem: true } }), +); +// [{}, {}, { key: 'value' }] ``` +Supported preservation contexts are: + +- `root`: the cleaned input value itself. +- `objectProperty`: a value assigned to an object property. +- `arrayItem`: a value contained directly in an array. + ### `preserveNullishArrays` Optional boolean. diff --git a/src/index.ts b/src/index.ts index 6e3becc..fe6ea9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,16 +10,23 @@ function isEmptyArray(arr: unknown) { return Array.isArray(arr) && arr.length === 0; } +type PreservationContext = 'arrayItem' | 'objectProperty' | 'root'; + +type PreservationRule = boolean | Partial>; + interface RemovalOptions { - preserveEmptyArray?: boolean; - preserveEmptyObject?: boolean; - preserveEmptyObjectsInArrays?: boolean; + preserveEmptyArray?: PreservationRule; + preserveEmptyObject?: PreservationRule; preserveNullishArrays?: boolean; removeAllFalsy?: boolean; } -function shouldPreserveEmptyObjectInArray(options: RemovalOptions) { - return options.preserveEmptyObjectsInArrays ?? options.preserveEmptyObject; +function shouldPreserve(rule: PreservationRule | undefined, context: PreservationContext) { + if (typeof rule === 'boolean') { + return rule; + } + + return rule?.[context] ?? false; } // Remove objects that has undefined value or recursively contain undefined values @@ -77,9 +84,9 @@ function stripEmptyObjects(obj: any, options: RemovalOptions = {}) { value = stripEmptyObjects(value, options); - if (isEmptyObject(value) && !options.preserveEmptyObject) { + if (isEmptyObject(value) && !shouldPreserve(options.preserveEmptyObject, 'objectProperty')) { delete cleanObj[key]; - } else if (isEmptyArray(value) && !options.preserveEmptyArray) { + } else if (isEmptyArray(value) && !shouldPreserve(options.preserveEmptyArray, 'objectProperty')) { delete cleanObj[key]; } else { cleanObj[key] = value; @@ -94,9 +101,9 @@ function stripEmptyObjects(obj: any, options: RemovalOptions = {}) { if (typeof value === 'object' && value !== null) { value = stripEmptyObjects(value, options); - if (isEmptyObject(value) && !shouldPreserveEmptyObjectInArray(options)) { + if (isEmptyObject(value) && !shouldPreserve(options.preserveEmptyObject, 'arrayItem')) { delete cleanObj[idx]; - } else if (isEmptyArray(value) && !options.preserveEmptyArray) { + } else if (isEmptyArray(value) && !shouldPreserve(options.preserveEmptyArray, 'arrayItem')) { delete cleanObj[idx]; } else { cleanObj[idx] = value; @@ -128,8 +135,8 @@ export default function removeUndefinedObjects(obj?: T, options?: RemovalOpti // If the only thing that's leftover is an empty object or empty array then return nothing. if ( - (isEmptyObject(withoutUndefined) && !options?.preserveEmptyObject) || - (isEmptyArray(withoutUndefined) && !options?.preserveEmptyArray) + (isEmptyObject(withoutUndefined) && !shouldPreserve(options?.preserveEmptyObject, 'root')) || + (isEmptyArray(withoutUndefined) && !shouldPreserve(options?.preserveEmptyArray, 'root')) ) { return; } diff --git a/test/index.test.ts b/test/index.test.ts index a524c16..77e8012 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -181,13 +181,17 @@ it('should not remove empty object values from arrays when preserveEmptyObject i }); }); -it('should remove empty object values from arrays when preserveEmptyObjectsInArrays is false', () => { +it('should preserve empty object values by context', () => { + expect(removeUndefinedObjects({}, { preserveEmptyObject: { root: true } })).toStrictEqual({}); + expect(removeUndefinedObjects({ value: {} }, { preserveEmptyObject: { root: true } })).toStrictEqual({}); + expect(removeUndefinedObjects({}, { preserveEmptyObject: { objectProperty: true } })).toBeUndefined(); + expect( - removeUndefinedObjects([{}, { a: undefined }, { b: 'b' }], { - preserveEmptyObject: true, - preserveEmptyObjectsInArrays: false, - }), - ).toStrictEqual([{ b: 'b' }]); + removeUndefinedObjects( + { value: {}, nested: { value: undefined } }, + { preserveEmptyObject: { objectProperty: true } }, + ), + ).toStrictEqual({ value: {}, nested: {} }); expect( removeUndefinedObjects( @@ -199,8 +203,10 @@ it('should remove empty object values from arrays when preserveEmptyObjectsInArr array: [{}, { emptyObject: {} }, { value: 'value' }], }, { - preserveEmptyObject: true, - preserveEmptyObjectsInArrays: false, + preserveEmptyObject: { + objectProperty: true, + root: true, + }, }, ), ).toStrictEqual({ @@ -216,22 +222,84 @@ it('should remove empty object values from arrays when preserveEmptyObjectsInArr { value: [{}, { a: undefined }] }, { preserveEmptyArray: true, - preserveEmptyObject: true, - preserveEmptyObjectsInArrays: false, + preserveEmptyObject: { + objectProperty: true, + root: true, + }, }, ), ).toStrictEqual({ value: [] }); -}); -it('should preserve empty object values from arrays when preserveEmptyObjectsInArrays is true', () => { expect( removeUndefinedObjects([{}, { a: undefined }, { b: 'b' }], { - preserveEmptyObject: false, - preserveEmptyObjectsInArrays: true, + preserveEmptyObject: { arrayItem: true }, }), ).toStrictEqual([{}, {}, { b: 'b' }]); }); +it('should preserve empty arrays by context', () => { + expect(removeUndefinedObjects([], { preserveEmptyArray: { root: true } })).toStrictEqual([]); + expect(removeUndefinedObjects({ value: [] }, { preserveEmptyArray: { root: true } })).toBeUndefined(); + expect(removeUndefinedObjects([], { preserveEmptyArray: { objectProperty: true } })).toBeUndefined(); + + expect( + removeUndefinedObjects( + { value: [], nested: { value: [undefined] } }, + { preserveEmptyArray: { objectProperty: true } }, + ), + ).toStrictEqual({ value: [], nested: { value: [] } }); + + expect( + removeUndefinedObjects([[], [undefined], ['value']], { + preserveEmptyArray: { arrayItem: true }, + }), + ).toStrictEqual([[], [], ['value']]); +}); + +it('should preserve empty objects and arrays by context when both maps are supplied', () => { + expect( + removeUndefinedObjects( + { + emptyArrayProperty: [], + emptyObjectProperty: {}, + nested: { + emptyArrayProperty: [undefined], + emptyObjectProperty: { value: undefined }, + }, + array: [ + {}, + [], + { + emptyArrayProperty: [], + emptyObjectProperty: {}, + }, + ['value'], + { value: 'value' }, + ], + }, + { + preserveEmptyArray: { objectProperty: true }, + preserveEmptyObject: { objectProperty: true }, + }, + ), + ).toStrictEqual({ + emptyArrayProperty: [], + emptyObjectProperty: {}, + nested: { + emptyArrayProperty: [], + emptyObjectProperty: {}, + }, + array: [ + { + emptyArrayProperty: [], + emptyObjectProperty: {}, + }, + ['value'], + { value: 'value' }, + ], + }); +}); + it('should not remove null values from arrays when preserveArrayNulls is true', () => { expect(removeUndefinedObjects([null], { preserveNullishArrays: true })).toStrictEqual([null]); expect(removeUndefinedObjects([undefined], { preserveNullishArrays: true })).toBeUndefined(); From b370c404efae5428f5a1ca11759835a2c793f452 Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Tue, 26 May 2026 15:03:53 -0700 Subject: [PATCH 3/3] Delete .changeset/empty-object-array-items.md --- .changeset/empty-object-array-items.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/empty-object-array-items.md diff --git a/.changeset/empty-object-array-items.md b/.changeset/empty-object-array-items.md deleted file mode 100644 index 45eb112..0000000 --- a/.changeset/empty-object-array-items.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'remove-undefined-objects': minor ---- - -Allow `preserveEmptyObject` and `preserveEmptyArray` to preserve empty values by context.