Skip to content

Commit 0784f63

Browse files
feat!: allow filtering out values before merging them
by default, undefined is now filtered out. fix #460
1 parent 9cc2acc commit 0784f63

17 files changed

+358
-71
lines changed

docs/API.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ If set to a function, that function will be used to merge everything else.
5959

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

62+
#### `filterValues`
63+
64+
Type: `false | (values: unknown[], meta: MetaData) => unknown[]`
65+
66+
If `false`, no values will be filter out. If set to a function, that function will be used to filter values.
67+
By default, `undefined` values will be filtered out (`null` values will be kept).
68+
6269
### `rootMetaData`
6370

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

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

154+
#### `filterValues`
155+
156+
Type: `false | (values: unknown[], meta: MetaData) => unknown[]`
157+
158+
If `false`, no values will be filter out. If set to a function, that function will be used to filter values.
159+
By default, `undefined` values will be filtered out (`null` values will be kept).
160+
147161
### `rootMetaData`
148162

149163
Type: `MetaData`

docs/deepmergeCustom.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,78 @@ type EveryIsDate<Ts extends ReadonlyArray<unknown>> = Ts extends readonly [
237237
Note: If you want to use HKTs in your own project, not related to deepmerge-ts, we recommend checking out
238238
[fp-ts](https://gcanti.github.io/fp-ts/modules/HKT.ts.html).
239239

240+
## Filtering Values
241+
242+
You can filter the values before they are merged by using the `filterValues` option.
243+
By default, we filter out all `undefined` values.
244+
245+
If you don't want to filter out any values, you can set the `filterValues` option to `false`.
246+
Be sure to also set the `DeepMergeFilterValuesURI` to `DeepMergeNoFilteringURI` to ensure correct return types.
247+
248+
```ts
249+
import {
250+
type DeepMergeMergeFunctionURItoKind,
251+
type DeepMergeMergeFunctionsURIs,
252+
type DeepMergeNoFilteringURI,
253+
deepmergeCustom,
254+
} from "deepmerge-ts";
255+
256+
const customizedDeepmerge = deepmergeCustom<
257+
unknown,
258+
{
259+
DeepMergeFilterValuesURI: DeepMergeNoFilteringURI;
260+
}
261+
>({
262+
filterValues: false,
263+
});
264+
265+
const x = { key1: { subkey1: `one` } };
266+
const y = { key1: undefined };
267+
const z = { key1: { subkey2: `two` } };
268+
269+
customizedDeepmerge(x, y, z); // => { key1: { subkey2: `two` } }
270+
```
271+
272+
Here's an example that creates a custom deepmerge function that filters out all `null` values instead of `undefined`.
273+
274+
<!-- eslint-disable ts/no-shadow -->
275+
276+
```ts
277+
import {
278+
type DeepMergeMergeFunctionURItoKind,
279+
type DeepMergeMergeFunctionsURIs,
280+
type FilterOut,
281+
deepmergeCustom,
282+
} from "deepmerge-ts";
283+
284+
const customizedDeepmerge = deepmergeCustom<
285+
unknown,
286+
{
287+
DeepMergeFilterValuesURI: "FilterNullValues";
288+
}
289+
>({
290+
filterValues(values, meta) {
291+
return values.filter((value) => value !== null);
292+
},
293+
});
294+
295+
const x = { key1: { subkey1: `one` } };
296+
const y = { key1: null };
297+
const z = { key1: { subkey2: `two` } };
298+
299+
customizedDeepmerge(x, y, z); // => { key1: { subkey1: `one`, subkey2: `two` } }
300+
301+
declare module "deepmerge-ts" {
302+
interface DeepMergeMergeFunctionURItoKind<
303+
Ts extends Readonly<ReadonlyArray<unknown>>,
304+
MF extends DeepMergeMergeFunctionsURIs,
305+
M,
306+
> {
307+
readonly FilterNullValues: FilterOut<Ts, null>;
308+
}
309+
}
310+
```
311+
240312
## Meta Data
241313

242314
We provide a simple object of meta data that states the key that the values being merged were under.

src/deepmerge-into.ts

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { actionsInto as actions } from "./actions";
2+
import {
3+
defaultFilterValues,
4+
defaultMetaDataUpdater,
5+
} from "./defaults/general";
26
import * as defaultMergeIntoFunctions from "./defaults/into";
3-
import { defaultMetaDataUpdater } from "./defaults/meta-data-updater";
47
import {
58
type DeepMergeBuiltInMetaData,
69
type DeepMergeHKT,
@@ -174,6 +177,10 @@ function getIntoUtils<
174177
MM
175178
>["metaDataUpdater"],
176179
deepmergeInto: customizedDeepmergeInto,
180+
filterValues:
181+
options.filterValues === false
182+
? undefined
183+
: options.filterValues ?? defaultFilterValues,
177184
actions,
178185
};
179186
}
@@ -196,30 +203,42 @@ export function mergeUnknownsInto<
196203
meta: M | undefined,
197204
// eslint-disable-next-line ts/no-invalid-void-type
198205
): void | symbol {
199-
if (values.length === 0) {
206+
const filteredValues = utils.filterValues?.(values, meta) ?? values;
207+
208+
if (filteredValues.length === 0) {
200209
return;
201210
}
202-
if (values.length === 1) {
203-
return void mergeOthersInto<U, M, MM>(m_target, values, utils, meta);
211+
if (filteredValues.length === 1) {
212+
return void mergeOthersInto<U, M, MM>(
213+
m_target,
214+
filteredValues,
215+
utils,
216+
meta,
217+
);
204218
}
205219

206220
const type = getObjectType(m_target.value);
207221

208222
if (type !== ObjectType.NOT && type !== ObjectType.OTHER) {
209-
for (let m_index = 1; m_index < values.length; m_index++) {
210-
if (getObjectType(values[m_index]) === type) {
223+
for (let m_index = 1; m_index < filteredValues.length; m_index++) {
224+
if (getObjectType(filteredValues[m_index]) === type) {
211225
continue;
212226
}
213227

214-
return void mergeOthersInto<U, M, MM>(m_target, values, utils, meta);
228+
return void mergeOthersInto<U, M, MM>(
229+
m_target,
230+
filteredValues,
231+
utils,
232+
meta,
233+
);
215234
}
216235
}
217236

218237
switch (type) {
219238
case ObjectType.RECORD: {
220239
return void mergeRecordsInto<U, M, MM>(
221240
m_target as Reference<Record<PropertyKey, unknown>>,
222-
values as ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
241+
filteredValues as ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
223242
utils,
224243
meta,
225244
);
@@ -228,7 +247,7 @@ export function mergeUnknownsInto<
228247
case ObjectType.ARRAY: {
229248
return void mergeArraysInto<U, M, MM>(
230249
m_target as Reference<unknown[]>,
231-
values as ReadonlyArray<ReadonlyArray<unknown>>,
250+
filteredValues as ReadonlyArray<ReadonlyArray<unknown>>,
232251
utils,
233252
meta,
234253
);
@@ -237,7 +256,7 @@ export function mergeUnknownsInto<
237256
case ObjectType.SET: {
238257
return void mergeSetsInto<U, M, MM>(
239258
m_target as Reference<Set<unknown>>,
240-
values as ReadonlyArray<Readonly<ReadonlySet<unknown>>>,
259+
filteredValues as ReadonlyArray<Readonly<ReadonlySet<unknown>>>,
241260
utils,
242261
meta,
243262
);
@@ -246,14 +265,21 @@ export function mergeUnknownsInto<
246265
case ObjectType.MAP: {
247266
return void mergeMapsInto<U, M, MM>(
248267
m_target as Reference<Map<unknown, unknown>>,
249-
values as ReadonlyArray<Readonly<ReadonlyMap<unknown, unknown>>>,
268+
filteredValues as ReadonlyArray<
269+
Readonly<ReadonlyMap<unknown, unknown>>
270+
>,
250271
utils,
251272
meta,
252273
);
253274
}
254275

255276
default: {
256-
return void mergeOthersInto<U, M, MM>(m_target, values, utils, meta);
277+
return void mergeOthersInto<U, M, MM>(
278+
m_target,
279+
filteredValues,
280+
utils,
281+
meta,
282+
);
257283
}
258284
}
259285
}

src/deepmerge.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { actions } from "./actions";
2-
import { defaultMetaDataUpdater } from "./defaults/meta-data-updater";
2+
import {
3+
defaultFilterValues,
4+
defaultMetaDataUpdater,
5+
} from "./defaults/general";
36
import * as defaultMergeFunctions from "./defaults/vanilla";
47
import {
58
type DeepMergeBuiltInMetaData,
@@ -139,6 +142,10 @@ function getUtils<
139142
>["metaDataUpdater"],
140143
deepmerge: customizedDeepmerge,
141144
useImplicitDefaultMerging: options.enableImplicitDefaultMerging ?? false,
145+
filterValues:
146+
options.filterValues === false
147+
? undefined
148+
: options.filterValues ?? defaultFilterValues,
142149
actions,
143150
};
144151
}
@@ -155,26 +162,28 @@ export function mergeUnknowns<
155162
M,
156163
MM extends DeepMergeBuiltInMetaData = DeepMergeBuiltInMetaData,
157164
>(values: Ts, utils: U, meta: M | undefined): DeepMergeHKT<Ts, MF, M> {
158-
if (values.length === 0) {
165+
const filteredValues = utils.filterValues?.(values, meta) ?? values;
166+
167+
if (filteredValues.length === 0) {
159168
return undefined as DeepMergeHKT<Ts, MF, M>;
160169
}
161-
if (values.length === 1) {
162-
return mergeOthers<U, M, MM>(values, utils, meta) as DeepMergeHKT<
170+
if (filteredValues.length === 1) {
171+
return mergeOthers<U, M, MM>(filteredValues, utils, meta) as DeepMergeHKT<
163172
Ts,
164173
MF,
165174
M
166175
>;
167176
}
168177

169-
const type = getObjectType(values[0]);
178+
const type = getObjectType(filteredValues[0]);
170179

171180
if (type !== ObjectType.NOT && type !== ObjectType.OTHER) {
172-
for (let m_index = 1; m_index < values.length; m_index++) {
173-
if (getObjectType(values[m_index]) === type) {
181+
for (let m_index = 1; m_index < filteredValues.length; m_index++) {
182+
if (getObjectType(filteredValues[m_index]) === type) {
174183
continue;
175184
}
176185

177-
return mergeOthers<U, M, MM>(values, utils, meta) as DeepMergeHKT<
186+
return mergeOthers<U, M, MM>(filteredValues, utils, meta) as DeepMergeHKT<
178187
Ts,
179188
MF,
180189
M
@@ -185,38 +194,40 @@ export function mergeUnknowns<
185194
switch (type) {
186195
case ObjectType.RECORD: {
187196
return mergeRecords<U, MF, M, MM>(
188-
values as ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
197+
filteredValues as ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
189198
utils,
190199
meta,
191200
) as DeepMergeHKT<Ts, MF, M>;
192201
}
193202

194203
case ObjectType.ARRAY: {
195204
return mergeArrays<U, M, MM>(
196-
values as ReadonlyArray<Readonly<ReadonlyArray<unknown>>>,
205+
filteredValues as ReadonlyArray<Readonly<ReadonlyArray<unknown>>>,
197206
utils,
198207
meta,
199208
) as DeepMergeHKT<Ts, MF, M>;
200209
}
201210

202211
case ObjectType.SET: {
203212
return mergeSets<U, M, MM>(
204-
values as ReadonlyArray<Readonly<ReadonlySet<unknown>>>,
213+
filteredValues as ReadonlyArray<Readonly<ReadonlySet<unknown>>>,
205214
utils,
206215
meta,
207216
) as DeepMergeHKT<Ts, MF, M>;
208217
}
209218

210219
case ObjectType.MAP: {
211220
return mergeMaps<U, M, MM>(
212-
values as ReadonlyArray<Readonly<ReadonlyMap<unknown, unknown>>>,
221+
filteredValues as ReadonlyArray<
222+
Readonly<ReadonlyMap<unknown, unknown>>
223+
>,
213224
utils,
214225
meta,
215226
) as DeepMergeHKT<Ts, MF, M>;
216227
}
217228

218229
default: {
219-
return mergeOthers<U, M, MM>(values, utils, meta) as DeepMergeHKT<
230+
return mergeOthers<U, M, MM>(filteredValues, utils, meta) as DeepMergeHKT<
220231
Ts,
221232
MF,
222233
M

src/defaults/general.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { type DeepMergeBuiltInMetaData } from "../types";
2+
3+
/**
4+
* The default function to update meta data.
5+
*
6+
* It doesn't update the meta data.
7+
*/
8+
export function defaultMetaDataUpdater<M>(
9+
previousMeta: M,
10+
metaMeta: DeepMergeBuiltInMetaData,
11+
): DeepMergeBuiltInMetaData {
12+
return metaMeta;
13+
}
14+
15+
/**
16+
* The default function to filter values.
17+
*
18+
* It filters out undefined values.
19+
*/
20+
export function defaultFilterValues<Ts extends ReadonlyArray<unknown>, M>(
21+
values: Ts,
22+
meta: M | undefined,
23+
): unknown[] {
24+
return values.filter((value) => value !== undefined);
25+
}

src/defaults/into.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,5 @@ export function mergeOthers<Ts extends ReadonlyArray<unknown>>(
121121
m_target: Reference<unknown>,
122122
values: Ts,
123123
) {
124-
for (let i = values.length - 1; i >= 0; i--) {
125-
if (values[i] !== undefined) {
126-
m_target.value = values[i];
127-
return;
128-
}
129-
}
130-
m_target.value = undefined;
124+
m_target.value = values.at(-1);
131125
}

src/defaults/meta-data-updater.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/defaults/vanilla.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,5 @@ export function mergeMaps<
122122
* Get the last non-undefined value in the given array.
123123
*/
124124
export function mergeOthers<Ts extends ReadonlyArray<unknown>>(values: Ts) {
125-
for (let i = values.length - 1; i >= 0; i--) {
126-
if (values[i] !== undefined) {
127-
return values[i];
128-
}
129-
}
130-
return undefined;
125+
return values.at(-1);
131126
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type {
1515
DeepMergeHKT,
1616
DeepMergeLeaf,
1717
DeepMergeLeafURI,
18+
DeepMergeNoFilteringURI,
1819
DeepMergeMapsDefaultHKT,
1920
DeepMergeMergeFunctionsDefaultURIs,
2021
DeepMergeMergeFunctionsURIs,
@@ -28,3 +29,4 @@ export type {
2829
Reference as DeepMergeValueReference,
2930
GetDeepMergeMergeFunctionsURIs,
3031
} from "./types";
32+
export type { FilterOut } from "./types/utils";

0 commit comments

Comments
 (0)