Skip to content

Commit

Permalink
feat!: allow filtering out values before merging them
Browse files Browse the repository at this point in the history
by default, undefined is now filtered out. fix #460
  • Loading branch information
RebeccaStevens committed May 20, 2024
1 parent 6477b91 commit 65574a3
Show file tree
Hide file tree
Showing 17 changed files with 358 additions and 71 deletions.
14 changes: 14 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ 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.

#### `filterValues`

Type: `false | (values: unknown[], meta: MetaData) => unknown[]`

If `false`, no values will be filter out. If set to a function, that function will be used to filter values.
By default, `undefined` values will be filtered out (`null` values will be kept).

### `rootMetaData`

Type: `MetaData`
Expand Down Expand Up @@ -144,6 +151,13 @@ If set to a function, that function will be used to merge everything else by mut

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

#### `filterValues`

Type: `false | (values: unknown[], meta: MetaData) => unknown[]`

If `false`, no values will be filter out. If set to a function, that function will be used to filter values.
By default, `undefined` values will be filtered out (`null` values will be kept).

### `rootMetaData`

Type: `MetaData`
Expand Down
72 changes: 72 additions & 0 deletions docs/deepmergeCustom.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,78 @@ type EveryIsDate<Ts extends ReadonlyArray<unknown>> = Ts extends readonly [
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).

## Filtering Values

You can filter the values before they are merged by using the `filterValues` option.
By default, we filter out all `undefined` values.

If you don't want to filter out any values, you can set the `filterValues` option to `false`.
Be sure to also set the `DeepMergeFilterValuesURI` to `DeepMergeNoFilteringURI` to ensure correct return types.

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

const customizedDeepmerge = deepmergeCustom<
unknown,
{
DeepMergeFilterValuesURI: DeepMergeNoFilteringURI;
}
>({
filterValues: false,
});

const x = { key1: { subkey1: `one` } };
const y = { key1: undefined };
const z = { key1: { subkey2: `two` } };

customizedDeepmerge(x, y, z); // => { key1: { subkey2: `two` } }
```

Here's an example that creates a custom deepmerge function that filters out all `null` values instead of `undefined`.

<!-- eslint-disable ts/no-shadow -->

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

const customizedDeepmerge = deepmergeCustom<
unknown,
{
DeepMergeFilterValuesURI: "FilterNullValues";
}
>({
filterValues(values, meta) {
return values.filter((value) => value !== null);
},
});

const x = { key1: { subkey1: `one` } };
const y = { key1: null };
const z = { key1: { subkey2: `two` } };

customizedDeepmerge(x, y, z); // => { key1: { subkey1: `one`, subkey2: `two` } }

declare module "deepmerge-ts" {
interface DeepMergeMergeFunctionURItoKind<
Ts extends Readonly<ReadonlyArray<unknown>>,
MF extends DeepMergeMergeFunctionsURIs,
M,
> {
readonly FilterNullValues: FilterOut<Ts, null>;
}
}
```

## Meta Data

We provide a simple object of meta data that states the key that the values being merged were under.
Expand Down
50 changes: 38 additions & 12 deletions src/deepmerge-into.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { actionsInto as actions } from "./actions";
import {
defaultFilterValues,
defaultMetaDataUpdater,
} from "./defaults/general";
import * as defaultMergeIntoFunctions from "./defaults/into";
import { defaultMetaDataUpdater } from "./defaults/meta-data-updater";
import {
type DeepMergeBuiltInMetaData,
type DeepMergeHKT,
Expand Down Expand Up @@ -174,6 +177,10 @@ function getIntoUtils<
MM
>["metaDataUpdater"],
deepmergeInto: customizedDeepmergeInto,
filterValues:
options.filterValues === false
? undefined
: options.filterValues ?? defaultFilterValues,
actions,
};
}
Expand All @@ -196,30 +203,42 @@ export function mergeUnknownsInto<
meta: M | undefined,
// eslint-disable-next-line ts/no-invalid-void-type
): void | symbol {
if (values.length === 0) {
const filteredValues = utils.filterValues?.(values, meta) ?? values;

if (filteredValues.length === 0) {
return;
}
if (values.length === 1) {
return void mergeOthersInto<U, M, MM>(m_target, values, utils, meta);
if (filteredValues.length === 1) {
return void mergeOthersInto<U, M, MM>(
m_target,
filteredValues,
utils,
meta,
);
}

const type = getObjectType(m_target.value);

if (type !== ObjectType.NOT && type !== ObjectType.OTHER) {
for (let m_index = 1; m_index < values.length; m_index++) {
if (getObjectType(values[m_index]) === type) {
for (let m_index = 1; m_index < filteredValues.length; m_index++) {
if (getObjectType(filteredValues[m_index]) === type) {
continue;
}

return void mergeOthersInto<U, M, MM>(m_target, values, utils, meta);
return void mergeOthersInto<U, M, MM>(
m_target,
filteredValues,
utils,
meta,
);
}
}

switch (type) {
case ObjectType.RECORD: {
return void mergeRecordsInto<U, M, MM>(
m_target as Reference<Record<PropertyKey, unknown>>,
values as ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
filteredValues as ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
utils,
meta,
);
Expand All @@ -228,7 +247,7 @@ export function mergeUnknownsInto<
case ObjectType.ARRAY: {
return void mergeArraysInto<U, M, MM>(
m_target as Reference<unknown[]>,
values as ReadonlyArray<ReadonlyArray<unknown>>,
filteredValues as ReadonlyArray<ReadonlyArray<unknown>>,
utils,
meta,
);
Expand All @@ -237,7 +256,7 @@ export function mergeUnknownsInto<
case ObjectType.SET: {
return void mergeSetsInto<U, M, MM>(
m_target as Reference<Set<unknown>>,
values as ReadonlyArray<Readonly<ReadonlySet<unknown>>>,
filteredValues as ReadonlyArray<Readonly<ReadonlySet<unknown>>>,
utils,
meta,
);
Expand All @@ -246,14 +265,21 @@ export function mergeUnknownsInto<
case ObjectType.MAP: {
return void mergeMapsInto<U, M, MM>(
m_target as Reference<Map<unknown, unknown>>,
values as ReadonlyArray<Readonly<ReadonlyMap<unknown, unknown>>>,
filteredValues as ReadonlyArray<
Readonly<ReadonlyMap<unknown, unknown>>
>,
utils,
meta,
);
}

default: {
return void mergeOthersInto<U, M, MM>(m_target, values, utils, meta);
return void mergeOthersInto<U, M, MM>(
m_target,
filteredValues,
utils,
meta,
);
}
}
}
Expand Down
37 changes: 24 additions & 13 deletions src/deepmerge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { actions } from "./actions";
import { defaultMetaDataUpdater } from "./defaults/meta-data-updater";
import {
defaultFilterValues,
defaultMetaDataUpdater,
} from "./defaults/general";
import * as defaultMergeFunctions from "./defaults/vanilla";
import {
type DeepMergeBuiltInMetaData,
Expand Down Expand Up @@ -139,6 +142,10 @@ function getUtils<
>["metaDataUpdater"],
deepmerge: customizedDeepmerge,
useImplicitDefaultMerging: options.enableImplicitDefaultMerging ?? false,
filterValues:
options.filterValues === false
? undefined
: options.filterValues ?? defaultFilterValues,
actions,
};
}
Expand All @@ -155,26 +162,28 @@ export function mergeUnknowns<
M,
MM extends DeepMergeBuiltInMetaData = DeepMergeBuiltInMetaData,
>(values: Ts, utils: U, meta: M | undefined): DeepMergeHKT<Ts, MF, M> {
if (values.length === 0) {
const filteredValues = utils.filterValues?.(values, meta) ?? values;

if (filteredValues.length === 0) {
return undefined as DeepMergeHKT<Ts, MF, M>;
}
if (values.length === 1) {
return mergeOthers<U, M, MM>(values, utils, meta) as DeepMergeHKT<
if (filteredValues.length === 1) {
return mergeOthers<U, M, MM>(filteredValues, utils, meta) as DeepMergeHKT<
Ts,
MF,
M
>;
}

const type = getObjectType(values[0]);
const type = getObjectType(filteredValues[0]);

if (type !== ObjectType.NOT && type !== ObjectType.OTHER) {
for (let m_index = 1; m_index < values.length; m_index++) {
if (getObjectType(values[m_index]) === type) {
for (let m_index = 1; m_index < filteredValues.length; m_index++) {
if (getObjectType(filteredValues[m_index]) === type) {
continue;
}

return mergeOthers<U, M, MM>(values, utils, meta) as DeepMergeHKT<
return mergeOthers<U, M, MM>(filteredValues, utils, meta) as DeepMergeHKT<
Ts,
MF,
M
Expand All @@ -185,38 +194,40 @@ export function mergeUnknowns<
switch (type) {
case ObjectType.RECORD: {
return mergeRecords<U, MF, M, MM>(
values as ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
filteredValues as ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
utils,
meta,
) as DeepMergeHKT<Ts, MF, M>;
}

case ObjectType.ARRAY: {
return mergeArrays<U, M, MM>(
values as ReadonlyArray<Readonly<ReadonlyArray<unknown>>>,
filteredValues as ReadonlyArray<Readonly<ReadonlyArray<unknown>>>,
utils,
meta,
) as DeepMergeHKT<Ts, MF, M>;
}

case ObjectType.SET: {
return mergeSets<U, M, MM>(
values as ReadonlyArray<Readonly<ReadonlySet<unknown>>>,
filteredValues as ReadonlyArray<Readonly<ReadonlySet<unknown>>>,
utils,
meta,
) as DeepMergeHKT<Ts, MF, M>;
}

case ObjectType.MAP: {
return mergeMaps<U, M, MM>(
values as ReadonlyArray<Readonly<ReadonlyMap<unknown, unknown>>>,
filteredValues as ReadonlyArray<
Readonly<ReadonlyMap<unknown, unknown>>
>,
utils,
meta,
) as DeepMergeHKT<Ts, MF, M>;
}

default: {
return mergeOthers<U, M, MM>(values, utils, meta) as DeepMergeHKT<
return mergeOthers<U, M, MM>(filteredValues, utils, meta) as DeepMergeHKT<
Ts,
MF,
M
Expand Down
25 changes: 25 additions & 0 deletions src/defaults/general.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { type DeepMergeBuiltInMetaData } from "../types";

/**
* The default function to update meta data.
*
* It doesn't update the meta data.
*/
export function defaultMetaDataUpdater<M>(
previousMeta: M,
metaMeta: DeepMergeBuiltInMetaData,
): DeepMergeBuiltInMetaData {
return metaMeta;
}

/**
* The default function to filter values.
*
* It filters out undefined values.
*/
export function defaultFilterValues<Ts extends ReadonlyArray<unknown>, M>(
values: Ts,
meta: M | undefined,
): unknown[] {
return values.filter((value) => value !== undefined);
}
8 changes: 1 addition & 7 deletions src/defaults/into.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,5 @@ export function mergeOthers<Ts extends ReadonlyArray<unknown>>(
m_target: Reference<unknown>,
values: Ts,
) {
for (let i = values.length - 1; i >= 0; i--) {
if (values[i] !== undefined) {
m_target.value = values[i];
return;
}
}
m_target.value = undefined;
m_target.value = values.at(-1);
}
11 changes: 0 additions & 11 deletions src/defaults/meta-data-updater.ts

This file was deleted.

7 changes: 1 addition & 6 deletions src/defaults/vanilla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,5 @@ export function mergeMaps<
* Get the last non-undefined value in the given array.
*/
export function mergeOthers<Ts extends ReadonlyArray<unknown>>(values: Ts) {
for (let i = values.length - 1; i >= 0; i--) {
if (values[i] !== undefined) {
return values[i];
}
}
return undefined;
return values.at(-1);
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type {
DeepMergeHKT,
DeepMergeLeaf,
DeepMergeLeafURI,
DeepMergeNoFilteringURI,
DeepMergeMapsDefaultHKT,
DeepMergeMergeFunctionsDefaultURIs,
DeepMergeMergeFunctionsURIs,
Expand All @@ -28,3 +29,4 @@ export type {
Reference as DeepMergeValueReference,
GetDeepMergeMergeFunctionsURIs,
} from "./types";
export type { FilterOut } from "./types/utils";
Loading

0 comments on commit 65574a3

Please sign in to comment.