From ea86ec79ba1e61b5a6406c88dc7f0223c352f9be Mon Sep 17 00:00:00 2001 From: eagletrhost Date: Wed, 10 Sep 2025 20:49:53 +1000 Subject: [PATCH 1/5] refactor: use own function to remove undefined & stop removing nulls in arrays --- src/index.ts | 38 +++++++++++++++++++++++++++++++------- test/index.test.ts | 6 ++++-- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 75e01e0..c508da5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,32 @@ interface RemovalOptions { removeAllFalsy?: boolean; } +// Remove objects that has undefined value or recursively contain undefined values +function removeUndefined(obj: any): any { + if (obj === undefined) { + return undefined; + } + // Preserve null + if (obj === null) { + return null; + } + // Remove undefined in arrays + if (Array.isArray(obj)) { + return obj.map(removeUndefined).filter(item => item !== undefined); + } + if (typeof obj === 'object') { + const cleaned: any = {}; + Object.entries(obj).forEach(([key, value]) => { + const cleanedValue = removeUndefined(value); + if (cleanedValue !== undefined) { + cleaned[key] = cleanedValue; + } + }); + return cleaned; + } + return obj; +} + // Modified from here: https://stackoverflow.com/a/43781499 function stripEmptyObjects(obj: any, options: RemovalOptions = {}) { const cleanObj = obj; @@ -59,8 +85,7 @@ function stripEmptyObjects(obj: any, options: RemovalOptions = {}) { } else { cleanObj[idx] = value; } - } else if (value === null) { - // Null entries within an array should be removed. + } else if (value === null && options.removeAllFalsy) { delete cleanObj[idx]; } }); @@ -75,12 +100,11 @@ export default function removeUndefinedObjects(obj?: T, options?: RemovalOpti return undefined; } - // JSON.stringify removes undefined values. Though `[undefined]` will be converted with this to - // `[null]`, we'll clean that up next. - // eslint-disable-next-line try-catch-failsafe/json-parse - let withoutUndefined = JSON.parse(JSON.stringify(obj)); + // Remove objects that recursively contain undefined values + // E.g. { a: { b: undefined } } -> { a: {} } + let withoutUndefined = removeUndefined(obj); - // Then we recursively remove all empty objects and nullish arrays. + // Then we recursively remove all empty objects and nullish arrays withoutUndefined = stripEmptyObjects(withoutUndefined, options); // If the only thing that's leftover is an empty object then return nothing. diff --git a/test/index.test.ts b/test/index.test.ts index 8e529d5..2b69d29 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -77,6 +77,7 @@ test('should remove empty arrays from within object', () => { expect(removeUndefinedObjects(obj)).toStrictEqual({ d: [1234], f: null, + g: [null, null], }); }); @@ -99,11 +100,12 @@ test('should remove empty arrays and falsy values from within object when remove }); }); -test('should remove undefined and null values from arrays', () => { +test('should remove undefined values from arrays & not null values', () => { expect(removeUndefinedObjects([undefined, undefined])).toBeUndefined(); - expect(removeUndefinedObjects([null])).toBeUndefined(); + expect(removeUndefinedObjects([null])).toStrictEqual([null]); expect(removeUndefinedObjects(['1234', null, undefined, { a: null, b: undefined }, ' ', ''])).toStrictEqual([ '1234', + null, { a: null, }, From 5d5659e7f731346d9a21d2a50a40157e74d15e40 Mon Sep 17 00:00:00 2001 From: eagletrhost Date: Thu, 11 Sep 2025 10:20:11 +1000 Subject: [PATCH 2/5] feat: make remove array nulls optional --- README.md | 12 ++++++++++++ src/index.ts | 14 +++++++++----- test/index.test.ts | 21 +++++++++++++++++---- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 75c3029..25dfe42 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,18 @@ The following items will NOT be removed: ## Options +### `preserveArrayNulls` + +Optional boolean. +If provided, null values in arrays will be preserved instead of being removed. + +```js +import removeUndefinedObjects from 'remove-undefined-objects'; + +console.log(removeUndefinedObjects({ key1: [null, undefined], key2: 123 }, { preserveArrayNulls: true })); +// { key1: [null], key2: 123 } +``` + ### `removeAllFalsy` Optional boolean. diff --git a/src/index.ts b/src/index.ts index ca5a068..1a7e7d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,10 +7,12 @@ function isEmptyObject(obj: unknown) { } interface RemovalOptions { + preserveArrayNulls?: boolean; removeAllFalsy?: boolean; } // Remove objects that has undefined value or recursively contain undefined values +// biome-ignore lint/suspicious/noExplicitAny: This method does its own type assertions. function removeUndefined(obj: any): any { if (obj === undefined) { return undefined; @@ -24,7 +26,8 @@ function removeUndefined(obj: any): any { return obj.map(removeUndefined).filter(item => item !== undefined); } if (typeof obj === 'object') { - const cleaned: any = {}; + // biome-ignore lint/suspicious/noExplicitAny: We're just passing around the object values + const cleaned: Record = {}; Object.entries(obj).forEach(([key, value]) => { const cleanedValue = removeUndefined(value); if (cleanedValue !== undefined) { @@ -86,7 +89,7 @@ function stripEmptyObjects(obj: any, options: RemovalOptions = {}) { } else { cleanObj[idx] = value; } - } else if (value === null && options.removeAllFalsy) { + } else if (value === null && (options.removeAllFalsy || !options.preserveArrayNulls)) { delete cleanObj[idx]; } }); @@ -101,9 +104,10 @@ export default function removeUndefinedObjects(obj?: T, options?: RemovalOpti return undefined; } - // Remove objects that recursively contain undefined values - // E.g. { a: { b: undefined } } -> { a: {} } - let withoutUndefined = removeUndefined(obj); + // If array nulls are preserved, use the custom removeUndefined function so that + // undefined values in arrays aren't converted to nulls, which stringify does + // If we're not preserving array nulls (default behavior), it doesn't matter that the undefined array values are converted to nulls + let withoutUndefined = options?.preserveArrayNulls ? removeUndefined(obj) : JSON.parse(JSON.stringify(obj)); // Then we recursively remove all empty objects and nullish arrays withoutUndefined = stripEmptyObjects(withoutUndefined, options); diff --git a/test/index.test.ts b/test/index.test.ts index 34ab58b..2428d36 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -77,7 +77,6 @@ test('should remove empty arrays from within object', () => { expect(removeUndefinedObjects(obj)).toStrictEqual({ d: [1234], f: null, - g: [null, null], }); }); @@ -100,12 +99,11 @@ test('should remove empty arrays and falsy values from within object when remove }); }); -test('should remove undefined values from arrays & not null values', () => { +test('should remove undefined and null values from arrays', () => { expect(removeUndefinedObjects([undefined, undefined])).toBeUndefined(); - expect(removeUndefinedObjects([null])).toStrictEqual([null]); + expect(removeUndefinedObjects([null])).toBeUndefined(); expect(removeUndefinedObjects(['1234', null, undefined, { a: null, b: undefined }, ' ', ''])).toStrictEqual([ '1234', - null, { a: null, }, @@ -113,3 +111,18 @@ test('should remove undefined values from arrays & not null values', () => { '', ]); }); + +test('should not remove null values from arrays when preserveArrayNulls is true', () => { + expect(removeUndefinedObjects([null], { preserveArrayNulls: true })).toStrictEqual([null]); + expect(removeUndefinedObjects([undefined], { preserveArrayNulls: true })).toBeUndefined(); + expect(removeUndefinedObjects([null, undefined], { preserveArrayNulls: true })).toStrictEqual([null]); + expect( + removeUndefinedObjects([null, undefined, { a: null, b: undefined }], { preserveArrayNulls: true }), + ).toStrictEqual([null, { a: null }]); + expect( + removeUndefinedObjects( + { a: 'a', empty_nested: { nested2: { nested3: undefined } }, nested_array: { b: [null, 1, undefined, 2] } }, + { preserveArrayNulls: true }, + ), + ).toStrictEqual({ a: 'a', nested_array: { b: [null, 1, 2] } }); +}); From ae79151cc3e0f7f350ca86525aab26d34e822d7e Mon Sep 17 00:00:00 2001 From: eagletrhost Date: Thu, 11 Sep 2025 10:29:22 +1000 Subject: [PATCH 3/5] doc: add more example in readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 25dfe42..0671d65 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,10 @@ If provided, null values in arrays will be preserved instead of being removed. ```js import removeUndefinedObjects from 'remove-undefined-objects'; -console.log(removeUndefinedObjects({ key1: [null, undefined], key2: 123 }, { preserveArrayNulls: true })); -// { key1: [null], key2: 123 } +console.log(removeUndefinedObjects({ key1: [null, undefined], key2: 123, key3: null })); +// { key2: 123, key3: null } +console.log(removeUndefinedObjects({ key1: [null, undefined], key2: 123, key3: null }, { preserveArrayNulls: true })); +// { key1: [null], key2: 123, key3: null } ``` ### `removeAllFalsy` From 9d84f021c17501afd3419a8bf03fa523d1e7c84c Mon Sep 17 00:00:00 2001 From: eagletrhost Date: Fri, 12 Sep 2025 06:32:55 +1000 Subject: [PATCH 4/5] style: refactor naming & comments --- README.md | 6 ++++-- src/index.ts | 7 ++++--- test/index.test.ts | 10 +++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0671d65..fc3b5df 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The following items will NOT be removed: ## Options -### `preserveArrayNulls` +### `preserveNullishArrays` Optional boolean. If provided, null values in arrays will be preserved instead of being removed. @@ -44,7 +44,9 @@ import removeUndefinedObjects from 'remove-undefined-objects'; console.log(removeUndefinedObjects({ key1: [null, undefined], key2: 123, key3: null })); // { key2: 123, key3: null } -console.log(removeUndefinedObjects({ key1: [null, undefined], key2: 123, key3: null }, { preserveArrayNulls: true })); +console.log( + removeUndefinedObjects({ key1: [null, undefined], key2: 123, key3: null }, { preserveNullishArrays: true }), +); // { key1: [null], key2: 123, key3: null } ``` diff --git a/src/index.ts b/src/index.ts index 1a7e7d1..d535543 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ function isEmptyObject(obj: unknown) { } interface RemovalOptions { - preserveArrayNulls?: boolean; + preserveNullishArrays?: boolean; removeAllFalsy?: boolean; } @@ -89,7 +89,8 @@ function stripEmptyObjects(obj: any, options: RemovalOptions = {}) { } else { cleanObj[idx] = value; } - } else if (value === null && (options.removeAllFalsy || !options.preserveArrayNulls)) { + } else if (value === null && (options.removeAllFalsy || !options.preserveNullishArrays)) { + // Null entries within an array should be removed by default, unless explicitly preserved delete cleanObj[idx]; } }); @@ -107,7 +108,7 @@ export default function removeUndefinedObjects(obj?: T, options?: RemovalOpti // If array nulls are preserved, use the custom removeUndefined function so that // undefined values in arrays aren't converted to nulls, which stringify does // If we're not preserving array nulls (default behavior), it doesn't matter that the undefined array values are converted to nulls - let withoutUndefined = options?.preserveArrayNulls ? removeUndefined(obj) : JSON.parse(JSON.stringify(obj)); + let withoutUndefined = options?.preserveNullishArrays ? removeUndefined(obj) : JSON.parse(JSON.stringify(obj)); // Then we recursively remove all empty objects and nullish arrays withoutUndefined = stripEmptyObjects(withoutUndefined, options); diff --git a/test/index.test.ts b/test/index.test.ts index 2428d36..e9b9c6b 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -113,16 +113,16 @@ test('should remove undefined and null values from arrays', () => { }); test('should not remove null values from arrays when preserveArrayNulls is true', () => { - expect(removeUndefinedObjects([null], { preserveArrayNulls: true })).toStrictEqual([null]); - expect(removeUndefinedObjects([undefined], { preserveArrayNulls: true })).toBeUndefined(); - expect(removeUndefinedObjects([null, undefined], { preserveArrayNulls: true })).toStrictEqual([null]); + expect(removeUndefinedObjects([null], { preserveNullishArrays: true })).toStrictEqual([null]); + expect(removeUndefinedObjects([undefined], { preserveNullishArrays: true })).toBeUndefined(); + expect(removeUndefinedObjects([null, undefined], { preserveNullishArrays: true })).toStrictEqual([null]); expect( - removeUndefinedObjects([null, undefined, { a: null, b: undefined }], { preserveArrayNulls: true }), + removeUndefinedObjects([null, undefined, { a: null, b: undefined }], { preserveNullishArrays: true }), ).toStrictEqual([null, { a: null }]); expect( removeUndefinedObjects( { a: 'a', empty_nested: { nested2: { nested3: undefined } }, nested_array: { b: [null, 1, undefined, 2] } }, - { preserveArrayNulls: true }, + { preserveNullishArrays: true }, ), ).toStrictEqual({ a: 'a', nested_array: { b: [null, 1, 2] } }); }); From 9b575bd83758520f077277a0260fc394eaa8e265 Mon Sep 17 00:00:00 2001 From: eagletrhost Date: Fri, 12 Sep 2025 10:30:46 +1000 Subject: [PATCH 5/5] docs: update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 95ddf4d..169a717 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Optional boolean. If provided, empty arrays `[]` will not get removed ```js +import removeUndefinedObjects from 'remove-undefined-objects'; + console.log(removeUndefinedObjects({ key1: [], key2: [undefined], nested: { key3: 'a', key4: [] } })); // { nested: { key3: 'a' } }