@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
JavaScript
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;
}