@tienedev/datype
Version:
Modern TypeScript utility library with pragmatic typing and zero dependencies
80 lines (78 loc) • 2.67 kB
JavaScript
function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) {
return false;
}
if (Array.isArray(obj)) {
return false;
}
if (obj instanceof Date || obj instanceof RegExp) {
return false;
}
const proto = Object.getPrototypeOf(obj);
return proto === null || proto === Object.prototype;
}
const DEFAULT_OPTIONS = {
arrayMergeStrategy: 'concat',
maxDepth: 50,
};
function mergeInternal(target, sources, options, depth = 0, seen = new WeakSet()) {
if (depth > options.maxDepth) {
throw new Error(`Maximum merge depth (${options.maxDepth}) exceeded`);
}
if (seen.has(target)) {
return target;
}
seen.add(target);
const result = { ...target };
for (const source of sources) {
if (!isPlainObject(source)) {
continue;
}
for (const key of Object.keys(source)) {
const sourceValue = source[key];
const targetValue = result[key];
if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result[key] =
options.arrayMergeStrategy === 'concat'
? [...targetValue, ...sourceValue]
: sourceValue;
}
else if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result[key] = mergeInternal(targetValue, [sourceValue], options, depth + 1, seen);
}
else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result[key] = sourceValue;
}
}
}
seen.delete(target);
return result;
}
function deepMerge(target, ...sourcesAndOptions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) {
if (!isPlainObject(target)) {
throw new TypeError('Target must be a plain object');
}
if (sourcesAndOptions.length === 0) {
return { ...target };
}
let options = {};
let sources = [];
const lastArg = sourcesAndOptions[sourcesAndOptions.length - 1];
if (lastArg &&
typeof lastArg === 'object' &&
('arrayMergeStrategy' in lastArg || 'maxDepth' in lastArg)) {
options = lastArg;
sources = sourcesAndOptions.slice(0, -1);
}
else {
sources = sourcesAndOptions;
}
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
return mergeInternal(target, sources, mergedOptions);
}
export { deepMerge };