feat(utils): mergeDeepObject with dot notation

This commit is contained in:
Marek Polak
2024-05-14 14:18:41 +02:00
committed by martin
parent 1680300a5a
commit 1a5b0e2a0f
2 changed files with 53 additions and 9 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
// code shamelessly stolen from https://github.com/voodoocreation/ts-deepmerge
type TAllKeys<T> = T extends any ? keyof T : never;
@@ -25,7 +26,7 @@ type TMerged<T> = [T] extends [Array<any>]
: T;
// istanbul ignore next
const isObject = (obj: any) => {
const isObject = (obj: any): obj is IObject => {
if (typeof obj === 'object' && obj !== null) {
if (typeof Object.getPrototypeOf === 'function') {
const prototype = Object.getPrototypeOf(obj);
@@ -43,6 +44,28 @@ interface IObject {
[key: string]: any;
}
const mergeValuesWithPath = (target: any, value: any, [key, ...rest]: string[]): any => {
if (key === undefined) {
return mergeValues(target, value);
} else if (!isObject(target)) {
return { [key]: mergeValuesWithPath({}, value, rest) };
} else {
return { ...target, [key]: mergeValuesWithPath(target[key], value, rest) };
}
};
const mergeValues = (target: any, value: any) => {
if (Array.isArray(target) && Array.isArray(value)) {
return mergeDeepObject.options.mergeArrays
? Array.from(new Set((target as unknown[]).concat(value)))
: value;
} else if (isObject(target) && isObject(value)) {
return mergeDeepObject(target, value);
} else {
return value;
}
};
export const mergeDeepObject = <T extends IObject[]>(...objects: T): TMerged<T[number]> =>
objects.reduce((result, current) => {
if (Array.isArray(current)) {
@@ -54,14 +77,11 @@ export const mergeDeepObject = <T extends IObject[]>(...objects: T): TMerged<T[n
return;
}
if (Array.isArray(result[key]) && Array.isArray(current[key])) {
result[key] = mergeDeepObject.options.mergeArrays
? Array.from(new Set((result[key] as unknown[]).concat(current[key])))
: current[key];
} else if (isObject(result[key]) && isObject(current[key])) {
result[key] = mergeDeepObject(result[key] as IObject, current[key] as IObject);
if (mergeDeepObject.options.dotNotation) {
const [first, ...rest] = key.split('.');
result[first] = mergeValuesWithPath(result[first], current[key], rest);
} else {
result[key] = current[key];
result[key] = mergeValues(result[key], current[key]);
}
});
@@ -70,17 +90,19 @@ export const mergeDeepObject = <T extends IObject[]>(...objects: T): TMerged<T[n
interface IOptions {
mergeArrays: boolean;
dotNotation: boolean;
}
const defaultOptions: IOptions = {
mergeArrays: true,
dotNotation: false,
};
mergeDeepObject.options = defaultOptions;
mergeDeepObject.withOptions = <T extends IObject[]>(options: Partial<IOptions>, ...objects: T) => {
mergeDeepObject.options = {
mergeArrays: true,
...defaultOptions,
...options,
};

View File

@@ -195,4 +195,26 @@ describe('mergeDeepObject', () => {
expect(value).toBe(1);
});
});
describe('dot notation', () => {
const fn = () => {};
const first = { a: { b: 1, c: 'foo' }, 'd.e': { f: null, 'g.h': 42 }, l: { m: [8] } };
const second = { 'a.b': 3, d: { 'e.f': fn, 'e.g': true }, 'i.j.k': undefined, 'l.m': [9] };
const third = { 'i.j': 'bar' };
it('dot notation off', () => {
const res = mergeDeepObject.withOptions({ mergeArrays: false }, first, second, third);
expect(res).toStrictEqual({ ...first, ...second, ...third });
});
it('dot notation on', () => {
const res = mergeDeepObject.withOptions({ dotNotation: true }, first, second, third);
expect(res).toStrictEqual({
a: { b: 3, c: 'foo' },
d: { e: { f: fn, g: true } },
i: { j: 'bar' },
l: { m: [8, 9] },
});
});
});
});