diff --git a/src/deepmerge.ts b/src/deepmerge.ts index fc899a150..5eb490420 100644 --- a/src/deepmerge.ts +++ b/src/deepmerge.ts @@ -1,10 +1,11 @@ import type { - DeepMerge, + DeepMergeDefault, DeepMergeArrays, DeepMergeMaps, DeepMergeRecords, DeepMergeSets, DeepMergeUnknowns, + MergeFn, Property, } from "./types"; import { getKeys, getObjectType, ObjectType, objectHasProperty } from "./utils"; @@ -16,7 +17,7 @@ import { getKeys, getObjectType, ObjectType, objectHasProperty } from "./utils"; */ export function deepmerge( ...objects: readonly [...Ts] -): DeepMerge; +): DeepMergeDefault; /** * Deeply merge two or more objects. @@ -29,14 +30,31 @@ export function deepmerge( export function deepmerge( ...objects: Readonly> ): unknown { - if (objects.length === 0) { - return {}; - } - if (objects.length === 1) { - return objects[0]; - } + return deepmergeCustom(deepmergeUnknowns)(...objects); +} + +/** + * Deeply merge two or more objects using the given merge function. + * + * @param mergeFn - The function used to merge any 2 elements. + */ +export function deepmergeCustom( + mergeFn: MergeFn +): (...objects: Readonly>) => unknown { + const customDeepmerge: ( + ...objects: Readonly> + ) => unknown = (...objects) => { + if (objects.length === 0) { + return {}; + } + if (objects.length === 1) { + return objects[0]; + } + + return objects.reduce(mergeFn); + }; - return objects.reduce(deepmergeUnknowns); + return customDeepmerge; } /** diff --git a/src/types.ts b/src/types.ts index 44bd0b5fe..29be31f2b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,15 +1,47 @@ /** - * Deep merge 1 or more types given in an array. + * The default way 1 or more types are deep merged. */ -export type DeepMerge = +export type DeepMergeDefault = Ts extends readonly [infer T1, ...unknown[]] ? Ts extends readonly [T1, infer T2, ...infer TRest] ? TRest extends Readonly> - ? DeepMergeUnknowns - : DeepMergeUnknowns> - : T1 + ? DeepMergeTwoTypesDefault + : DeepMergeTwoTypesDefault> + : never : never; +/** + * A function that merges 2 things. + */ +export type MergeFn = (x: T1, y: T2) => unknown; + +/** + * The default way 2 types are deep merged. + */ +type DeepMergeTwoTypesDefault = DeepMergeTwoTypestWithFunction< + T1, + T2, + (x: unknown, y: unknown) => DeepMergeUnknowns +>; + +/** + * Deep merge 2 types using the given merge function. + */ +type DeepMergeTwoTypestWithFunction< + T1, + T2, + M extends (x: unknown, y: unknown) => unknown +> = M extends (x: unknown, y: unknown) => infer R + ? LeafOrType + : never; + +/** + * Get a leaf from the 2 types if one is never, otherwise use the other type. + */ +type LeafOrType = Or, IsNever> extends true + ? Leaf + : T; + /** * Deep merge 2 types. */ diff --git a/tests/.eslintrc.json b/tests/.eslintrc.json index 780b21225..04e02643b 100644 --- a/tests/.eslintrc.json +++ b/tests/.eslintrc.json @@ -2,6 +2,7 @@ "plugins": ["ava"], "extends": ["plugin:ava/recommended"], "rules": { - "import/no-relative-parent-imports": "off" + "import/no-relative-parent-imports": "off", + "functional/no-throw-statement": "off" } } diff --git a/tests/deepmerge-custom.test.ts b/tests/deepmerge-custom.test.ts new file mode 100644 index 000000000..e455d3ec4 --- /dev/null +++ b/tests/deepmerge-custom.test.ts @@ -0,0 +1,23 @@ +import test from "ava"; + +import { deepmergeCustom } from "../src/deepmerge"; + +test("custom merge strings", (t) => { + const v = "a"; + const x = "b"; + const y = "c"; + const z = "d"; + + const expected = "a b c d"; + + const myDeepmerge = deepmergeCustom((object1, object2) => { + if (typeof object1 !== "string" || typeof object2 !== "string") { + throw new TypeError("Assertion error"); + } + return `${object1} ${object2}`; + }); + + const merged = myDeepmerge(v, x, y, z); + + t.deepEqual(merged, expected); +}); diff --git a/tests/deepmerge.test.ts b/tests/deepmerge.test.ts index b80645478..e02699b48 100644 --- a/tests/deepmerge.test.ts +++ b/tests/deepmerge.test.ts @@ -102,13 +102,15 @@ test(`can merge nested objects`, (t) => { }, }; + const merged = deepmerge(x, y); + t.deepEqual(x, { key1: { subkey1: `value1`, subkey2: `value2`, }, }); - t.deepEqual(deepmerge(x, y), expected); + t.deepEqual(merged, expected); }); test(`replaces simple prop with nested object`, (t) => {