Skip to content

Commit 1908bf9

Browse files
committed
Add deepMerge option: undefined
1 parent b5a5fe4 commit 1908bf9

File tree

2 files changed

+166
-2
lines changed

2 files changed

+166
-2
lines changed

collections/deep_merge.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export function deepMerge<
208208
arrays: "merge";
209209
sets: "merge";
210210
maps: "merge";
211+
undefined: "replace";
211212
},
212213
>(
213214
record: Readonly<T>,
@@ -224,14 +225,14 @@ function deepMergeInternal<
224225
arrays: "merge";
225226
sets: "merge";
226227
maps: "merge";
228+
undefined: "replace";
227229
},
228230
>(
229231
record: Readonly<T>,
230232
other: Readonly<U>,
231233
seen: Set<NonNullable<unknown>>,
232234
options?: Readonly<Options>,
233235
) {
234-
// Extract options
235236
// Clone left operand to avoid performing mutations in-place
236237
type Result = DeepMerge<T, U, Options>;
237238
const result: Partial<Result> = {};
@@ -252,7 +253,10 @@ function deepMergeInternal<
252253

253254
const a = record[key] as ResultMember;
254255

255-
if (!Object.hasOwn(other, key)) {
256+
if (
257+
!Object.hasOwn(other, key) ||
258+
(other[key] === undefined && options?.undefined === "ignore")
259+
) {
256260
result[key] = a;
257261

258262
continue;
@@ -285,6 +289,7 @@ function mergeObjects(
285289
arrays: "merge",
286290
sets: "merge",
287291
maps: "merge",
292+
undefined: "replace",
288293
},
289294
): Readonly<NonNullable<Record<string, unknown> | Iterable<unknown>>> {
290295
// Recursively merge mergeable objects
@@ -387,6 +392,18 @@ export type DeepMergeOptions = {
387392
* @default {"merge"}
388393
*/
389394
sets?: MergingStrategy;
395+
396+
/**
397+
* How to handle comparisons between non-`undefined` values and `undefined`.
398+
*
399+
* - If `"replace"`, the value in `other` is always chosen.
400+
* - If `"ignore"`, the value in `other` is only chosen if not `undefined`.
401+
*
402+
* In both cases, a value of `undefined` is chosen over an omitted value.
403+
*
404+
* @default {"replace"}
405+
*/
406+
undefined?: "replace" | "ignore";
390407
};
391408

392409
/**

collections/deep_merge_test.ts

+147
Original file line numberDiff line numberDiff line change
@@ -428,3 +428,150 @@ Deno.test("deepMerge() handles target object is not modified", () => {
428428
quux: new Set([1, 2, 3]),
429429
});
430430
});
431+
432+
Deno.test("deepMerge() handles number vs undefined", () => {
433+
assertEquals(
434+
deepMerge<{ a: number | undefined }>(
435+
{ a: 1 },
436+
{ a: undefined },
437+
{ undefined: "ignore" },
438+
),
439+
{ a: 1 },
440+
);
441+
assertEquals(
442+
deepMerge(
443+
{ a: 1 },
444+
{ a: undefined },
445+
{ undefined: "replace" },
446+
),
447+
{ a: undefined },
448+
);
449+
assertEquals(
450+
deepMerge(
451+
{ a: 1 },
452+
{ a: undefined },
453+
// Default is replace
454+
),
455+
{ a: undefined },
456+
);
457+
assertEquals(
458+
deepMerge(
459+
{ a: undefined },
460+
{ a: 1 },
461+
{ undefined: "ignore" },
462+
),
463+
{ a: 1 },
464+
);
465+
assertEquals(
466+
deepMerge(
467+
{ a: undefined },
468+
{ a: 1 },
469+
{ undefined: "replace" },
470+
),
471+
{ a: 1 },
472+
);
473+
assertEquals(
474+
deepMerge(
475+
{ a: undefined },
476+
{ a: 1 },
477+
// Default is replace
478+
),
479+
{ a: 1 },
480+
);
481+
482+
assertEquals(
483+
deepMerge(
484+
{ a: undefined },
485+
{ a: undefined },
486+
{ undefined: "ignore" },
487+
),
488+
{ a: undefined },
489+
);
490+
assertEquals(
491+
deepMerge(
492+
{ a: undefined },
493+
{ a: undefined },
494+
{ undefined: "replace" },
495+
),
496+
{ a: undefined },
497+
);
498+
assertEquals(
499+
deepMerge(
500+
{ a: undefined },
501+
{ a: undefined },
502+
// Default is replace
503+
),
504+
{ a: undefined },
505+
);
506+
});
507+
508+
Deno.test("deepMerge() handles mergeable vs undefined", () => {
509+
assertEquals<{ a: { b: number } | undefined }>(
510+
deepMerge(
511+
{ a: { b: 1 } },
512+
{ a: undefined },
513+
{ undefined: "ignore" },
514+
),
515+
{ a: { b: 1 } },
516+
);
517+
assertEquals(
518+
deepMerge(
519+
{ a: { b: 1 } },
520+
{ a: undefined },
521+
{ undefined: "replace" },
522+
),
523+
{ a: undefined },
524+
);
525+
526+
assertEquals(
527+
deepMerge<{ a: { b: number; c: number | undefined } }>(
528+
{ a: { b: 1, c: 2 } },
529+
{ a: { b: 1, c: undefined } },
530+
{ undefined: "ignore" },
531+
),
532+
{ a: { b: 1, c: 2 } },
533+
);
534+
assertEquals(
535+
deepMerge(
536+
{ a: { b: 1, c: 2 } },
537+
{ a: { b: 1, c: undefined } },
538+
{ undefined: "replace" },
539+
),
540+
{ a: { b: 1, c: undefined } },
541+
);
542+
});
543+
544+
Deno.test("deepMerge() handles undefined vs omitted", () => {
545+
assertEquals(
546+
deepMerge(
547+
{ a: undefined },
548+
{},
549+
{ undefined: "ignore" },
550+
),
551+
{ a: undefined },
552+
);
553+
assertEquals(
554+
deepMerge(
555+
{ a: undefined },
556+
{},
557+
{ undefined: "replace" },
558+
),
559+
{ a: undefined },
560+
);
561+
assertEquals(
562+
deepMerge(
563+
{},
564+
{ a: undefined },
565+
{ undefined: "ignore" },
566+
),
567+
{ a: undefined },
568+
);
569+
assertEquals(
570+
deepMerge(
571+
{},
572+
{ a: undefined },
573+
{ undefined: "replace" },
574+
),
575+
{ a: undefined },
576+
);
577+
});

0 commit comments

Comments
 (0)