All files deepMerge.ts

100% Statements 48/48
100% Branches 24/24
100% Functions 5/5
100% Lines 43/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 1458x 8x 8x       69x       146x       17x 2x             15x     15x 50x 2x           129x 15x 27x 2x         140x     69x             79x 17x 17x 69x   69x 2x 2x   67x   17x     62x   5x         57x 22x     35x                                                     8x   5x   5x 5x 5x   5x                                         8x   7x   7x 7x     7x   7x    
import { isDefined, isArray, isObject } from './guards';
import { uniq } from './uniq';
import { clone } from './clone';
import { DeepPartial, DeepPartialObject } from './types';
 
function isCyclic(object: unknown) {
  const seenObjects = new WeakMap(); // use to keep track of which objects have been seen.
  function detectCycle(obj: unknown) {
    // If 'obj' is an actual object (i.e., has the form of '{}'), check
    // if it's been seen already.
    if (
      isObject(obj) &&
      Object.prototype.toString.call(obj) == '[object Object]'
    ) {
      if (seenObjects.has(obj)) {
        return true;
      }
 
      // If 'obj' hasn't been seen, add it to 'seenObjects'.
      // Since 'obj' is used as a key, the value of 'seenObjects[obj]'
      // is irrelevant and can be set as literally anything you want. I
      // just went with 'undefined'.
      seenObjects.set(obj, undefined);
 
      // Recurse through the object, looking for more circular references.
      for (var key in obj) {
        if (detectCycle(obj[key])) {
          return true;
        }
      }
 
      // If 'obj' is an array, check if any of it's elements are
      // an object that has been seen already.
    } else if (Array.isArray(obj)) {
      for (var i in obj) {
        if (detectCycle(obj[i])) {
          return true;
        }
      }
    }
 
    return false;
  }
 
  return detectCycle(object);
}
 
/**
 * Merging @param a and @param b recursively @param a is most important
 */
function recursiveMerge(a: unknown, b: unknown): unknown {
  if (isObject(a) && isObject(b)) {
    const output = { ...a };
    const keys = uniq([...Object.keys(a), ...Object.keys(b)]);
    for (const k of keys) {
      // If object cyclic I will not try to merge objects
      if (isCyclic(a[k])) {
        output[k] = a[k];
        continue;
      }
      output[k] = recursiveMerge(a[k], b[k]);
    }
    return output;
  }
 
  if (isArray(a) && isArray(b)) {
    // This step will nicely merge primitive values but will leave objects as duplicates
    return uniq(a.concat(b));
  }
 
  // If we here we know that a is primitive value if it's defined we chose a over b
  // Unless this value is empty string
  if (isDefined(a)) {
    return a;
  }
 
  return b;
}
 
/**
 * Merging object from left to right
 *
 * @param target - value be preserved if possible.
 * @param sources - value be preserved if possible.
 * @description
 * Consider following
 *
 * -  `array + obj = array`
 * -  `obj + array = obj`
 * -  `obj + obj = obj` (recursively merged)
 * -  `array + array = array` (removes duplicates using Set)
 * -  `(truthy plain value) + ob = (truthy plain value)`
 * -  `(truthy plain value) + undefined = (truthy plain value)`
 * -  `A(truthy plain value) + B(truthy plain value) = A(truthy plain value)`
 * -  `undefined + B(truthy plain value) = B(truthy plain value)`
 * -  `null + B(truthy plain value) = B(truthy plain value)`
 * 
 *
 * Handles circular references
 * @category Utility
 */
export function deepMergeLeft<T extends object, X extends DeepPartial<T>>(data: T, ...source: X[]): T;
export function deepMergeLeft<T extends object>(...sources: T[]): T;
export function deepMergeLeft<T extends object>(
  target: T,
  ...sources: DeepPartialObject<T>[]
): T {
  let output = { ...target } as unknown;
  for (const source of sources) {
    output = recursiveMerge(output, source);
  }
  return output as any;
}
 
/**
 * Merging object from right to left
 *
 * @param target value will be replaced if possible.
 * @description
 * Consider following
 * -  `array + obj = obj`
 * -  `obj + array = array`
 * -  `obj + obj = obj` (recursively merged)
 * -  `array + array = array` (removes duplicates using Set)
 * -  `(truthy plain value) + undefined = (truthy plain value)`
 * -  `A(truthy plain value) + B(truthy plain value) = B(truthy plain value)`
 * Handles circular references
 * @category Utility
 */
 
export function deepMergeRight<T extends object, X extends DeepPartial<T>>(data: T, ...source: X[]): T;
export function deepMergeRight<T extends object>(...sources: T[]): T;
export function deepMergeRight<T extends object>(
  target: T,
  ...sources: DeepPartialObject<T>[]
): T {
  let output = clone(target) as unknown;
  for (const source of sources) {
    // Only difference from mergeDeepLeft
    // That source go first and output last
    output = recursiveMerge(source, output);
  }
  return output as any;
}