UNPKG

@qntm-code/utils

Version:

A collection of useful utility functions with associated TypeScript types. All functions have been unit tested.

114 lines (113 loc) 4.08 kB
import { isMergeableObject } from '../type-predicates/isMergableObject.js'; /** * Merges two objects x and y deeply, returning a new merged object with the elements from both x and y. * * If an element at the same key is present for both x and y, the value from y will appear in the result. * * Merging creates a new object, so that neither x or y is modified. * * Note: By default, arrays are merged by concatenating them. */ export function merge(x, y, options = {}) { if (!options.arrayMerge) { options.arrayMerge = defaultArrayMerge; } if (!options.isMergeableObject) { options.isMergeableObject = isMergeableObject; } /** * cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() implementations can use it. The caller may not replace * it. */ options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified; const xIsArray = Array.isArray(x); const yIsArray = Array.isArray(y); if (xIsArray !== yIsArray) { return cloneUnlessOtherwiseSpecified(y, options); } else if (xIsArray) { return options.arrayMerge(x, y, options); } else { return mergeObject(x, y, options); } } merge.all = function mergeAll(array, options) { if (!Array.isArray(array)) { throw new Error('First argument should be an array'); } let result = {}; for (let i = 0, l = array.length; i < l; i++) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument result = merge(result, array[i], options); } return result; }; function emptyTarget(value) { return Array.isArray(value) ? [] : {}; } function cloneUnlessOtherwiseSpecified(value, options) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return options.isMergeableObject(value) ? merge(emptyTarget(value), value, options) : value; } function defaultArrayMerge(x, y, options) { return x.concat(y).map(element => cloneUnlessOtherwiseSpecified(element, options)); } function getMergeFunction(key, options) { if (!options.customMerge) { return merge; } const customMerge = options.customMerge(key); return typeof customMerge === 'function' ? customMerge : merge; } function getEnumerableOwnPropertySymbols(target) { const symbols = Object.getOwnPropertySymbols(target); const result = []; for (const symbol of symbols) { if (Object.propertyIsEnumerable.call(target, symbol)) { result.push(symbol); } } return result; } function getKeys(target) { return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)); } function propertyIsOnObject(object, property) { try { return property in object; } catch (_) { return false; } } // Protects from prototype poisoning and unexpected merging up the prototype chain. function propertyIsUnsafe(target, key) { return (propertyIsOnObject(target, key) && // Properties are safe to merge if they don't exist in the target yet, !(Object.hasOwnProperty.call(target, key) && // unsafe if they exist up the prototype chain, Object.propertyIsEnumerable.call(target, key))); // and also unsafe if they're nonenumerable. } function mergeObject(x, y, options) { const destination = {}; if (options.isMergeableObject(x)) { const xKeys = getKeys(x); for (let i = 0, l = xKeys.length; i < l; i++) { const key = xKeys[i]; destination[key] = cloneUnlessOtherwiseSpecified(x[key], options); } } const yKeys = getKeys(y); for (let i = 0, l = yKeys.length; i < l; i++) { const key = yKeys[i]; if (propertyIsUnsafe(x, key)) { break; } if (propertyIsOnObject(x, key) && options.isMergeableObject(y[key])) { destination[key] = getMergeFunction(key, options)(x[key], y[key], options); } else { destination[key] = cloneUnlessOtherwiseSpecified(y[key], options); } } return destination; }