Skip to content

Commit

Permalink
feat: add basic support for custom merging
Browse files Browse the repository at this point in the history
  • Loading branch information
RebeccaStevens committed Aug 25, 2021
1 parent 38a9fff commit a843057
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 16 deletions.
36 changes: 27 additions & 9 deletions src/deepmerge.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type {
DeepMerge,
DeepMergeDefault,
DeepMergeArrays,
DeepMergeMaps,
DeepMergeRecords,
DeepMergeSets,
DeepMergeUnknowns,
MergeFn,
Property,
} from "./types";
import { getKeys, getObjectType, ObjectType, objectHasProperty } from "./utils";
Expand All @@ -16,7 +17,7 @@ import { getKeys, getObjectType, ObjectType, objectHasProperty } from "./utils";
*/
export function deepmerge<Ts extends readonly [unknown, ...unknown[]]>(
...objects: readonly [...Ts]
): DeepMerge<Ts>;
): DeepMergeDefault<Ts>;

/**
* Deeply merge two or more objects.
Expand All @@ -29,14 +30,31 @@ export function deepmerge(
export function deepmerge(
...objects: Readonly<ReadonlyArray<unknown>>
): 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<unknown, unknown>
): (...objects: Readonly<ReadonlyArray<unknown>>) => unknown {
const customDeepmerge: (
...objects: Readonly<ReadonlyArray<unknown>>
) => unknown = (...objects) => {
if (objects.length === 0) {
return {};
}
if (objects.length === 1) {
return objects[0];
}

return objects.reduce(mergeFn);
};

return objects.reduce(deepmergeUnknowns);
return customDeepmerge;
}

/**
Expand Down
42 changes: 37 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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<Ts extends readonly [unknown, ...unknown[]]> =
export type DeepMergeDefault<Ts extends readonly [unknown, ...unknown[]]> =
Ts extends readonly [infer T1, ...unknown[]]
? Ts extends readonly [T1, infer T2, ...infer TRest]
? TRest extends Readonly<ReadonlyArray<never>>
? DeepMergeUnknowns<T1, T2>
: DeepMergeUnknowns<T1, DeepMerge<[T2, ...TRest]>>
: T1
? DeepMergeTwoTypesDefault<T1, T2>
: DeepMergeTwoTypesDefault<T1, DeepMergeDefault<[T2, ...TRest]>>
: never
: never;

/**
* A function that merges 2 things.
*/
export type MergeFn<T1, T2> = (x: T1, y: T2) => unknown;

/**
* The default way 2 types are deep merged.
*/
type DeepMergeTwoTypesDefault<T1, T2> = DeepMergeTwoTypestWithFunction<
T1,
T2,
(x: unknown, y: unknown) => DeepMergeUnknowns<T1, T2>
>;

/**
* 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<T1, T2, R>
: never;

/**
* Get a leaf from the 2 types if one is never, otherwise use the other type.
*/
type LeafOrType<L1, L2, T> = Or<IsNever<L1>, IsNever<L2>> extends true
? Leaf<L1, L2>
: T;

/**
* Deep merge 2 types.
*/
Expand Down
3 changes: 2 additions & 1 deletion tests/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
23 changes: 23 additions & 0 deletions tests/deepmerge-custom.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
4 changes: 3 additions & 1 deletion tests/deepmerge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down

0 comments on commit a843057

Please sign in to comment.