Skip to content

Commit

Permalink
feat: add support for custom merging (#4)
Browse files Browse the repository at this point in the history
* feat: add support for custom merging

* feat: add support for typing custom merges

* feat: add support for easily turning off merging

* docs: create docs for deepmergeCustom
  • Loading branch information
RebeccaStevens committed Sep 13, 2021
1 parent ef432ad commit 5413b81
Show file tree
Hide file tree
Showing 20 changed files with 1,509 additions and 312 deletions.
5 changes: 5 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
],
"words": [
"deepmerge",
"deepmergecustomoptions",
"deepmergets",
"HKT",
"HKTs",
"kinded",
"typeguard",
"typeguards",
"foo",
Expand Down
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,7 @@
"/**/*.js",
"/**/*.md"
],
"rules": {}
"rules": {
"import/no-relative-parent-imports": "error"
}
}
117 changes: 100 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ yarn add -D deepmerge-ts
- Record merging support.
- Array merging support.
- Map and Set merging support.
- Customized merging.

## Usage

### Example
### Example using default config

```js
import { deepmerge } from "deepmerge-ts";
Expand Down Expand Up @@ -67,20 +68,102 @@ const y = {
]),
};

const output = {
record: {
prop1: "changed",
prop2: "value2",
prop3: "value3",
},
array: [1, 2, 3, 2, 3, 4],
set: new Set([1, 2, 3, 4]),
map: new Map([
["key1", "value1"],
["key2", "changed"],
["key3", "value3"],
]),
};

deepmerge(x, y) // => output
const merged = deepmerge(x, y);

console.log(merged);

// Prettierfied output:
//
// {
// record: {
// prop1: "changed",
// prop2: "value2",
// prop3: "value3"
// }
// array: (6) [1, 2, 3, 2, 3, 4]
// set: Set(4) {1, 2, 3, 4}
// map: Map(3) {
// "key1" => "value1",
// "key2" => "changed",
// "key3" => "value3"
// }
// }
```

### Using customized config

[See deepmerge custom docs](./docs/deepmergeCustom.md).

## API

### deepmerge(x, y, ...)

Merges the given inputs together using the default configuration.

#### deepmerge(...inputs)

Merges the array of inputs together using the default configuration.

Note: If `inputs` isn't typed as a tuple then we cannot determine the output type. The output type will simply be `unknown`.

### deepmergeCustom(options)

Generate a customized deepmerge function using the given options. The returned function works just like `deepmerge` except it uses the customized configuration.

#### options

The following options can be used to customize the deepmerge function.\
All these options are optional.

##### `mergeRecords`

Type: `false | (values: Record<any, unknown>[], utils: DeepMergeMergeFunctionUtils) => unknown`

If false, records won't be merged. If set to a function, that function will be used to merge records.

Note: Records are "vanilla" objects (e.g. `{ foo: "hello", bar: "world" }`).

##### `mergeArrays`

Type: `false | (values: unknown[][], utils: DeepMergeMergeFunctionUtils) => unknown`

If false, arrays won't be merged. If set to a function, that function will be used to merge arrays.

##### `mergeMaps`

Type: `false | (values: Map<unknown, unknown>[], utils: DeepMergeMergeFunctionUtils) => unknown`

If false, maps won't be merged. If set to a function, that function will be used to merge maps.

##### `mergeSets`

Type: `false | (values: Set<unknown>[], utils: DeepMergeMergeFunctionUtils) => unknown`

If false, sets won't be merged. If set to a function, that function will be used to merge sets.

##### `mergeOthers`

Type: `(values: Set<unknown>[], utils: DeepMergeMergeFunctionUtils) => unknown`

If set to a function, that function will be used to merge everything else.

Note: This includes merging mixed types, such as merging a map with an array.

#### DeepMergeMergeFunctionUtils

This is a set of utility functions that are made available to your custom merge functions.

##### `mergeFunctions`

These are all the merge function being used to perform the deepmerge.\
These will be the custom merge functions you gave, or the default merge functions for options you didn't customize.

##### `defaultMergeFunctions`

These are all the merge functions that the default, non-customize deepmerge function uses.

##### `deepmerge`

This is your top level customized deepmerge function.

Note: Be careful when calling this as it is really easy to end up in an infinite loop.
94 changes: 94 additions & 0 deletions docs/deepmergeCustom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Deepmerge Custom

`deepmergeCustom` allows you to customize the deepmerge function. It is a higher-order function; that is to say it returns a new customized deepmerge function.

## Customizing the return type

If you want to customize the deepmerge function, you probably also want the return type of the result to be correct too.\
Unfortunately however, due to TypeScript limitations, we can not automatically infer this.
In order to get the correct return type, you need to provide us with type information about how you have customized the function (we do the very same to define the default configuration).

We need to use HKTs (higher-kinded types) in order to generate the right output type. But again, unfortunately, TypeScript does not support HKTs. Luckily however, there is a workaround.
To use HKTs, we alias the type to a string type (a URI) and simply refer to that type by its alias until we need to resolve it.

Here's a simple example that creates a custom deepmerge function that does not merge arrays.

```js
import type { DeepMergeLeafURI } from "deepmerge-ts";
import { deepmergeCustom } from "deepmerge-ts";

const customDeepmerge = deepmergeCustom<{
DeepMergeArraysURI: DeepMergeLeafURI; // <-- Needed for correct output type.
}>({
mergeArrays: false,
});

const x = { foo: [1, 2], bar: [3, 4] };
const y = { foo: [5, 6] };

customDeepmerge(x, y); // => { foo: [5, 6], bar: [3, 4] }
```

When resolving a HKT, we use a lookup inside an interface called `DeepMergeMergeFunctionURItoKind`.
This interface needs to contain all the mappings of the URIs to their actual type.

When defining your own HKT for use with deepmerge, you need to extend this interface with your mapping.
This can be done using [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) by declaring a module block for this library and defining the same interface.

```ts
declare module "deepmerge-ts" {
interface DeepMergeMergeFunctionURItoKind<Ts extends ReadonlyArray<unknown>, MF extends DeepMergeMergeFunctionsURIs> {
readonly MyCustomMergeURI: MyValue;
}
}
```

Here's an example of creating a custom deepmerge function that amalgamates dates into an array.

```ts
import type { DeepMergeLeaf, DeepMergeMergeFunctionURItoKind, DeepMergeMergeFunctionsURIs } from "deepmerge-ts";
import { deepmergeCustom } from "deepmerge-ts";

const customizedDeepmerge = deepmergeCustom<{
DeepMergeOthersURI: "MyDeepMergeDatesURI"; // <-- Needed for correct output type.
}>({
mergeOthers: (values, utils) => {
// If every value is a date, the return the amalgamated array.
if (values.every((value) => value instanceof Date)) {
return values;
}
// Otherwise, use the default merging strategy.
return utils.defaultMergeFunctions.mergeOthers(values, utils);
},
});

const x = { foo: new Date("2020-01-01") };
const y = { foo: new Date("2021-02-02") };
const z = { foo: new Date("2022-03-03") };

customDeepmerge(x, y, z); // => { foo: [Date, Date, Date] }

declare module "deepmerge-ts" {
interface DeepMergeMergeFunctionURItoKind<
Ts extends ReadonlyArray<unknown>,
MF extends DeepMergeMergeFunctionsURIs
> {
readonly MyDeepMergeDatesURI: EveryIsDate<Ts> extends true ? Ts : DeepMergeLeaf<Ts>;
}
}

type EveryIsDate<Ts extends ReadonlyArray<unknown>> = Ts extends readonly [
infer Head,
...infer Rest
]
? Head extends Date
? EveryIsDate<Rest>
: false
: true;
```

Note: If you want to use HKTs in your own project, not related to deepmerge-ts, we recommend checking out [fp-ts](https://gcanti.github.io/fp-ts/modules/HKT.ts.html).

## API

[See deepmerge custom API](../README.md#deepmergecustomoptions).
7 changes: 6 additions & 1 deletion rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ const dts = {
format: "es",
},

plugins: [rollupPluginDts()],
plugins: [
rollupPluginTypescript({
tsconfig: "tsconfig.build.json",
}),
rollupPluginDts(),
],
};

export default [cjs, esm, dts];

0 comments on commit 5413b81

Please sign in to comment.