From 1908bf99b133bcc44ae34e7f52be9c69582b0c3e Mon Sep 17 00:00:00 2001 From: RShields Date: Thu, 27 Mar 2025 09:13:53 -0700 Subject: [PATCH 1/2] Add deepMerge option: undefined --- collections/deep_merge.ts | 21 ++++- collections/deep_merge_test.ts | 147 +++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 2 deletions(-) diff --git a/collections/deep_merge.ts b/collections/deep_merge.ts index 48b8acbaf6e2..b28985588d54 100644 --- a/collections/deep_merge.ts +++ b/collections/deep_merge.ts @@ -208,6 +208,7 @@ export function deepMerge< arrays: "merge"; sets: "merge"; maps: "merge"; + undefined: "replace"; }, >( record: Readonly, @@ -224,6 +225,7 @@ function deepMergeInternal< arrays: "merge"; sets: "merge"; maps: "merge"; + undefined: "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?.undefined === "ignore") + ) { result[key] = a; continue; @@ -285,6 +289,7 @@ function mergeObjects( arrays: "merge", sets: "merge", maps: "merge", + undefined: "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"} + */ + undefined?: "replace" | "ignore"; }; /** diff --git a/collections/deep_merge_test.ts b/collections/deep_merge_test.ts index cdcbb8a364d7..47ae4c5da9de 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 }, + { undefined: "ignore" }, + ), + { a: 1 }, + ); + assertEquals( + deepMerge( + { a: 1 }, + { a: undefined }, + { undefined: "replace" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + { a: 1 }, + { a: undefined }, + // Default is replace + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + { a: undefined }, + { a: 1 }, + { undefined: "ignore" }, + ), + { a: 1 }, + ); + assertEquals( + deepMerge( + { a: undefined }, + { a: 1 }, + { undefined: "replace" }, + ), + { a: 1 }, + ); + assertEquals( + deepMerge( + { a: undefined }, + { a: 1 }, + // Default is replace + ), + { a: 1 }, + ); + + assertEquals( + deepMerge( + { a: undefined }, + { a: undefined }, + { undefined: "ignore" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + { a: undefined }, + { a: undefined }, + { undefined: "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 }, + { undefined: "ignore" }, + ), + { a: { b: 1 } }, + ); + assertEquals( + deepMerge( + { a: { b: 1 } }, + { a: undefined }, + { undefined: "replace" }, + ), + { a: undefined }, + ); + + assertEquals( + deepMerge<{ a: { b: number; c: number | undefined } }>( + { a: { b: 1, c: 2 } }, + { a: { b: 1, c: undefined } }, + { undefined: "ignore" }, + ), + { a: { b: 1, c: 2 } }, + ); + assertEquals( + deepMerge( + { a: { b: 1, c: 2 } }, + { a: { b: 1, c: undefined } }, + { undefined: "replace" }, + ), + { a: { b: 1, c: undefined } }, + ); +}); + +Deno.test("deepMerge() handles undefined vs omitted", () => { + assertEquals( + deepMerge( + { a: undefined }, + {}, + { undefined: "ignore" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + { a: undefined }, + {}, + { undefined: "replace" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + {}, + { a: undefined }, + { undefined: "ignore" }, + ), + { a: undefined }, + ); + assertEquals( + deepMerge( + {}, + { a: undefined }, + { undefined: "replace" }, + ), + { a: undefined }, + ); +}); From 88605c0a2f5a5b0f0109e927f082e8d8e685d61a Mon Sep 17 00:00:00 2001 From: RShields Date: Thu, 10 Apr 2025 07:18:52 -0700 Subject: [PATCH 2/2] Rename undefined to undefineds --- collections/deep_merge.ts | 10 +++++----- collections/deep_merge_test.ts | 28 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/collections/deep_merge.ts b/collections/deep_merge.ts index b28985588d54..b698598f8155 100644 --- a/collections/deep_merge.ts +++ b/collections/deep_merge.ts @@ -208,7 +208,7 @@ export function deepMerge< arrays: "merge"; sets: "merge"; maps: "merge"; - undefined: "replace"; + undefineds: "replace"; }, >( record: Readonly, @@ -225,7 +225,7 @@ function deepMergeInternal< arrays: "merge"; sets: "merge"; maps: "merge"; - undefined: "replace"; + undefineds: "replace"; }, >( record: Readonly, @@ -255,7 +255,7 @@ function deepMergeInternal< if ( !Object.hasOwn(other, key) || - (other[key] === undefined && options?.undefined === "ignore") + (other[key] === undefined && options?.undefineds === "ignore") ) { result[key] = a; @@ -289,7 +289,7 @@ function mergeObjects( arrays: "merge", sets: "merge", maps: "merge", - undefined: "replace", + undefineds: "replace", }, ): Readonly | Iterable>> { // Recursively merge mergeable objects @@ -403,7 +403,7 @@ export type DeepMergeOptions = { * * @default {"replace"} */ - undefined?: "replace" | "ignore"; + undefineds?: "replace" | "ignore"; }; /** diff --git a/collections/deep_merge_test.ts b/collections/deep_merge_test.ts index 47ae4c5da9de..298a162ebd11 100644 --- a/collections/deep_merge_test.ts +++ b/collections/deep_merge_test.ts @@ -434,7 +434,7 @@ Deno.test("deepMerge() handles number vs undefined", () => { deepMerge<{ a: number | undefined }>( { a: 1 }, { a: undefined }, - { undefined: "ignore" }, + { undefineds: "ignore" }, ), { a: 1 }, ); @@ -442,7 +442,7 @@ Deno.test("deepMerge() handles number vs undefined", () => { deepMerge( { a: 1 }, { a: undefined }, - { undefined: "replace" }, + { undefineds: "replace" }, ), { a: undefined }, ); @@ -458,7 +458,7 @@ Deno.test("deepMerge() handles number vs undefined", () => { deepMerge( { a: undefined }, { a: 1 }, - { undefined: "ignore" }, + { undefineds: "ignore" }, ), { a: 1 }, ); @@ -466,7 +466,7 @@ Deno.test("deepMerge() handles number vs undefined", () => { deepMerge( { a: undefined }, { a: 1 }, - { undefined: "replace" }, + { undefineds: "replace" }, ), { a: 1 }, ); @@ -483,7 +483,7 @@ Deno.test("deepMerge() handles number vs undefined", () => { deepMerge( { a: undefined }, { a: undefined }, - { undefined: "ignore" }, + { undefineds: "ignore" }, ), { a: undefined }, ); @@ -491,7 +491,7 @@ Deno.test("deepMerge() handles number vs undefined", () => { deepMerge( { a: undefined }, { a: undefined }, - { undefined: "replace" }, + { undefineds: "replace" }, ), { a: undefined }, ); @@ -510,7 +510,7 @@ Deno.test("deepMerge() handles mergeable vs undefined", () => { deepMerge( { a: { b: 1 } }, { a: undefined }, - { undefined: "ignore" }, + { undefineds: "ignore" }, ), { a: { b: 1 } }, ); @@ -518,7 +518,7 @@ Deno.test("deepMerge() handles mergeable vs undefined", () => { deepMerge( { a: { b: 1 } }, { a: undefined }, - { undefined: "replace" }, + { undefineds: "replace" }, ), { a: undefined }, ); @@ -527,7 +527,7 @@ Deno.test("deepMerge() handles mergeable vs undefined", () => { deepMerge<{ a: { b: number; c: number | undefined } }>( { a: { b: 1, c: 2 } }, { a: { b: 1, c: undefined } }, - { undefined: "ignore" }, + { undefineds: "ignore" }, ), { a: { b: 1, c: 2 } }, ); @@ -535,7 +535,7 @@ Deno.test("deepMerge() handles mergeable vs undefined", () => { deepMerge( { a: { b: 1, c: 2 } }, { a: { b: 1, c: undefined } }, - { undefined: "replace" }, + { undefineds: "replace" }, ), { a: { b: 1, c: undefined } }, ); @@ -546,7 +546,7 @@ Deno.test("deepMerge() handles undefined vs omitted", () => { deepMerge( { a: undefined }, {}, - { undefined: "ignore" }, + { undefineds: "ignore" }, ), { a: undefined }, ); @@ -554,7 +554,7 @@ Deno.test("deepMerge() handles undefined vs omitted", () => { deepMerge( { a: undefined }, {}, - { undefined: "replace" }, + { undefineds: "replace" }, ), { a: undefined }, ); @@ -562,7 +562,7 @@ Deno.test("deepMerge() handles undefined vs omitted", () => { deepMerge( {}, { a: undefined }, - { undefined: "ignore" }, + { undefineds: "ignore" }, ), { a: undefined }, ); @@ -570,7 +570,7 @@ Deno.test("deepMerge() handles undefined vs omitted", () => { deepMerge( {}, { a: undefined }, - { undefined: "replace" }, + { undefineds: "replace" }, ), { a: undefined }, );