UNPKG

@snipsonian/core

Version:

Core/base reusable javascript code snippets

134 lines (116 loc) 4.31 kB
import isUndefined from '../is/isUndefined'; import isNull from '../is/isNull'; import isObjectPure from '../is/isObjectPure'; import cloneObjectDataProps from '../object/cloneObjectDataProps'; interface IMergeObjectPropsDeeplyOptions { ignoreUndefinedSourceProps?: boolean; /* default true */ ignoreNullSourceProps?: boolean; /* default true */ ignoreDifferentTypeSourceProps?: boolean; /* default true */ } /** * Merges the properties of two or more input objects deeply, returning a new object with the properties of all * source objects. * * If a property is present in multiple source objects, the value from the last inputted source will take precedence. * So the first source that is provided as input, is considered the "target" object. * For example: * prop of source A prop of source B result * ---------------- ---------------- ------ * {a: '123', c: 7} {a: 'z', b: 444} {a: 'z', b: 444, c: 7} * * But if the structure of the source properties is different (e.g. string vs. object) than the first source takes * precedence, except if the prop of that first source is null or undefined! * For example: * prop of source A prop of source B result * ---------------- ---------------- ------ * '123' (string) {b: 'zzz'} '123' * {a: 'abc'} 'xyz' (string) {a: 'abc'} * null {b: 'zzz'} {b: 'zzz'} * undefined {b: 'zzz'} {b: 'zzz'} * * p.s. * - some of the above behaviour can we tweaked by passing some options * - properties containing a function will not end up in the resulting object! */ export default function mergeObjectPropsDeeply(...sources: object[]): object { return mergeObjectPropsDeeplyOptionable({ sources, }); } export function mergeObjectPropsDeeplyOptionable({ options, sources, }: { options?: IMergeObjectPropsDeeplyOptions; sources: object[]; }): object { const initialValue = {}; return sources.reduce( (accumulator, source, index) => { if (index === 0) { return cloneObjectDataProps(source); } return mergeObjectPropsDeeplyFromSourceToTarget({ target: accumulator, source, options, }); }, initialValue, ); } export function mergeObjectPropsDeeplyFromSourceToTarget({ target, source, options, }: { target: object; source: object; options?: IMergeObjectPropsDeeplyOptions; }): object { const { ignoreUndefinedSourceProps = true, ignoreNullSourceProps = true, ignoreDifferentTypeSourceProps = true, } = options || {}; if (isUndefined(target) || isNull(target)) { return cloneProp(source); } if (isUndefined(source)) { return ignoreUndefinedSourceProps ? target : cloneProp(source); } if (isNull(source)) { return ignoreNullSourceProps ? target : cloneProp(source); } if ((typeof source !== typeof target)) { return ignoreDifferentTypeSourceProps ? target : cloneProp(source); } if (isObjectPure(target)) { if (isObjectPure(source)) { Object.keys(source).forEach((key) => { // eslint-disable-next-line no-param-reassign target[key] = mergeObjectPropsDeeplyFromSourceToTarget({ target: target[key] as object, source: source[key] as object, options, }); }); return target; } /* source is not a pure object like the target is but did still have the same type, so it is an array. */ return ignoreDifferentTypeSourceProps ? target : cloneProp(source); } return cloneProp(source); } // eslint-disable-next-line @typescript-eslint/no-explicit-any function cloneProp(prop: any): any { if (isUndefined(prop) || isNull(prop)) { return prop; } if (isObjectPure(prop)) { return cloneObjectDataProps(prop); } // eslint-disable-next-line @typescript-eslint/dot-notation return cloneObjectDataProps({ temp: prop })['temp']; }