diff --git a/README.md b/README.md index 9914e4c..169a717 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,22 @@ console.log( // { key1: [], key2: [], nested: { key3: 'a', key4: [] } } ``` +### `preserveNullishArrays` + +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, key3: null })); +// { key2: 123, key3: null } +console.log( + removeUndefinedObjects({ key1: [null, undefined], key2: 123, key3: null }, { preserveNullishArrays: true }), +); +// { key1: [null], key2: 123, key3: null } +``` + ### `removeAllFalsy` Optional boolean. diff --git a/src/index.ts b/src/index.ts index 986f93f..47299ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,9 +12,38 @@ function isEmptyArray(arr: unknown) { interface RemovalOptions { preserveEmptyArray?: boolean; + preserveNullishArrays?: 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; + } + // 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') { + // 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) { + cleaned[key] = cleanedValue; + } + }); + return cleaned; + } + return obj; +} + // Modified from here: https://stackoverflow.com/a/43781499 // biome-ignore lint/suspicious/noExplicitAny: This method does its own type assertions. function stripEmptyObjects(obj: any, options: RemovalOptions = {}) { @@ -69,8 +98,8 @@ 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 || !options.preserveNullishArrays)) { + // Null entries within an array should be removed by default, unless explicitly preserved delete cleanObj[idx]; } }); @@ -85,12 +114,12 @@ 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)); + // 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?.preserveNullishArrays ? removeUndefined(obj) : JSON.parse(JSON.stringify(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 or empty array then return nothing. diff --git a/test/index.test.ts b/test/index.test.ts index ace2336..bbfa1f2 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -141,3 +141,18 @@ test('should remove undefined and null values from arrays', () => { '', ]); }); + +test('should not remove null values from arrays when preserveArrayNulls is true', () => { + 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 }], { preserveNullishArrays: true }), + ).toStrictEqual([null, { a: null }]); + expect( + removeUndefinedObjects( + { a: 'a', empty_nested: { nested2: { nested3: undefined } }, nested_array: { b: [null, 1, undefined, 2] } }, + { preserveNullishArrays: true }, + ), + ).toStrictEqual({ a: 'a', nested_array: { b: [null, 1, 2] } }); +});