diff --git a/collections/deep_merge.ts b/collections/deep_merge.ts index 48b8acbaf6e2..b698598f8155 100644 --- a/collections/deep_merge.ts +++ b/collections/deep_merge.ts @@ -208,6 +208,7 @@ export function deepMerge< arrays: "merge"; sets: "merge"; maps: "merge"; + undefineds: "replace"; }, >( record: Readonly, @@ -224,6 +225,7 @@ function deepMergeInternal< arrays: "merge"; sets: "merge"; maps: "merge"; + undefineds: "replace"; }, >( record: Readonly, @@ -231,7 +233,6 @@ function deepMergeInternal< seen: Set>, options?: Readonly, ) { - // Extract options // Clone left operand to avoid performing mutations in-place type Result = DeepMerge; const result: Partial = {}; @@ -252,7 +253,10 @@ function deepMergeInternal< const a = record[key] as ResultMember; - if (!Object.hasOwn(other, key)) { + if ( + !Object.hasOwn(other, key) || + (other[key] === undefined && options?.undefineds === "ignore") + ) { result[key] = a; continue; @@ -285,6 +289,7 @@ function mergeObjects( arrays: "merge", sets: "merge", maps: "merge", + undefineds: "replace", }, ): Readonly | Iterable>> { // Recursively merge mergeable objects @@ -387,6 +392,18 @@ export type DeepMergeOptions = { * @default {"merge"} */ sets?: MergingStrategy; + + /** + * How to handle comparisons between non-`undefined` values and `undefined`. + * + * - If `"replace"`, the value in `other` is always chosen. + * - If `"ignore"`, the value in `other` is only chosen if not `undefined`. + * + * In both cases, a value of `undefined` is chosen over an omitted value. + * + * @default {"replace"} + */ + undefineds?: "replace" | "ignore"; }; /** diff --git a/collections/deep_merge_test.ts b/collections/deep_merge_test.ts index cdcbb8a364d7..298a162ebd11 100644 --- a/collections/deep_merge_test.ts +++ b/collections/deep_merge_test.ts @@ -428,3 +428,150 @@ Deno.test("deepMerge() handles target object is not modified", () => { quux: new Set([1, 2, 3]), }); }); + +Deno.test("deepMerge() handles number vs undefined", () => { + assertEquals( + deepMerge<{ a: number | undefined }>( + { a: 1 }, + { a: undefined }, + { undefineds: "ignore" }, + ), + { a: 1 }, + ); + assertEquals( + deepMerge( + { a: 1 }, + { a: undefined }, + { undefineds: "replace" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + { a: 1 }, + { a: undefined }, + // Default is replace + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + { a: undefined }, + { a: 1 }, + { undefineds: "ignore" }, + ), + { a: 1 }, + ); + assertEquals( + deepMerge( + { a: undefined }, + { a: 1 }, + { undefineds: "replace" }, + ), + { a: 1 }, + ); + assertEquals( + deepMerge( + { a: undefined }, + { a: 1 }, + // Default is replace + ), + { a: 1 }, + ); + + assertEquals( + deepMerge( + { a: undefined }, + { a: undefined }, + { undefineds: "ignore" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + { a: undefined }, + { a: undefined }, + { undefineds: "replace" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + { a: undefined }, + { a: undefined }, + // Default is replace + ), + { a: undefined }, + ); +}); + +Deno.test("deepMerge() handles mergeable vs undefined", () => { + assertEquals<{ a: { b: number } | undefined }>( + deepMerge( + { a: { b: 1 } }, + { a: undefined }, + { undefineds: "ignore" }, + ), + { a: { b: 1 } }, + ); + assertEquals( + deepMerge( + { a: { b: 1 } }, + { a: undefined }, + { undefineds: "replace" }, + ), + { a: undefined }, + ); + + assertEquals( + deepMerge<{ a: { b: number; c: number | undefined } }>( + { a: { b: 1, c: 2 } }, + { a: { b: 1, c: undefined } }, + { undefineds: "ignore" }, + ), + { a: { b: 1, c: 2 } }, + ); + assertEquals( + deepMerge( + { a: { b: 1, c: 2 } }, + { a: { b: 1, c: undefined } }, + { undefineds: "replace" }, + ), + { a: { b: 1, c: undefined } }, + ); +}); + +Deno.test("deepMerge() handles undefined vs omitted", () => { + assertEquals( + deepMerge( + { a: undefined }, + {}, + { undefineds: "ignore" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + { a: undefined }, + {}, + { undefineds: "replace" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + {}, + { a: undefined }, + { undefineds: "ignore" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + {}, + { a: undefined }, + { undefineds: "replace" }, + ), + { a: undefined }, + ); +});