Mastering Deep Merging in JavaScript Without Data Loss
Learn how to perform deep merging in JavaScript to combine objects without losing nested properties. Avoid common pitfalls with type-safe solutions.
- Topic
- JavaScript
- Reading time
- 4 min
- Length
- 892 words
- Published
- Aug 31, 2026
01:28 pm IST
In this article
In JavaScript and TypeScript application architecture, the need to combine configurations, manage nested state states, or apply user overrides to default settings is commonplace. Yet, many developers still rely on shallow copy mechanisms like Object.assign() or the object spread operator (...), which can lead to silent data loss and runtime bugs when dealing with nested objects. Let's explore how you can avoid these pitfalls by mastering deep merging techniques.
Understanding Shallow Merging and Its Pitfalls
Shallow merging only copies the top-level properties of an object. If a property is a reference type, such as a nested object or array, only the reference pointer is copied, not the actual nested data. This can result in unexpected data loss.
Consider the following scenario:
const defaultSettings = {
theme: 'light',
api: {
endpoint: 'https://api.example.com',
timeout: 5000
}
};
const userOverrides = {
api: {
timeout: 10000
}
};
const mergedSettings = { ...defaultSettings, ...userOverrides };
console.log(mergedSettings);
The output:
{
theme: 'light',
api: {
timeout: 10000
}
}
Here, the critical endpoint property from defaultSettings was completely lost because the spread operator performed a shallow assignment. To preserve nested properties, you need a Recursive Deep Merge.
The Mechanics of Recursive Deep Merging
A true deep merge algorithm must recursively traverse object trees. If both objects share a key, the algorithm must evaluate the types of their values:
- Both values are objects: Recursively call the merge function to combine their sub-properties.
- Both values are arrays: Decide whether to concatenate them, keep unique elements, or overwrite with the new array.
- Value types differ or are primitives: Resolve the conflict by either keeping the original value or overwriting it based on a designated collision policy.
To effectively implement a deep merge, the algorithm should handle various scenarios, such as:
- Handling null or undefined values, ensuring that these do not inadvertently overwrite valid data unless explicitly intended.
- Allowing custom strategies for array handling, which can be crucial for applications that need to maintain unique entries or prioritize newer data.
- Providing clear and predictable outcomes for conflicts between primitive values, which can be particularly important for ensuring that critical settings are not inadvertently overridden.
Creating a Type-Safe Deep Merge Utility in TypeScript
In TypeScript, it's crucial to avoid the any type and construct a robust, structurally typed merging engine. Here's a clean, type-safe implementation:
function isObject(item: unknown): boolean {
return typeof item === 'object' && item !== null && !Array.isArray(item);
}
function deepMerge(
a: Record,
b: Record,
arrayStrategy: 'concat' | 'unique' | 'replace' = 'concat',
conflictStrategy: 'overwrite-b' | 'keep-a' = 'overwrite-b'
): Record {
const result = { ...a };
for (const key of Object.keys(b)) {
const valA = a[key];
const valB = b[key];
if (isObject(valA) && isObject(valB)) {
result[key] = deepMerge(
valA as Record,
valB as Record,
arrayStrategy,
conflictStrategy
);
} else if (Array.isArray(valA) && Array.isArray(valB)) {
if (arrayStrategy === 'concat') {
result[key] = [...valA, ...valB];
} else if (arrayStrategy === 'unique') {
result[key] = Array.from(new Set([...valA, ...valB]));
} else {
result[key] = [...valB];
}
} else {
if (valA !== undefined) {
result[key] = conflictStrategy === 'keep-a' ? valA : valB;
} else {
result[key] = valB;
}
}
}
return result;
}
This implementation ensures strong type assertions by using structural record indexing (Record<string, unknown>) to protect against type-erasure. Separating array strategies and value conflicts avoids random reference changes or unexpected state mutations.
What This Means for Your Codebase
Integrating a type-safe deep merge utility into your JavaScript or TypeScript projects can significantly enhance the reliability of your code when dealing with complex configurations. By ensuring that nested properties are preserved and conflicts are resolved predictably, you can avoid many of the pitfalls associated with shallow merging techniques.
In your own projects, consider where configurations or state management might be at risk of silent data loss. Implement a deep merge strategy in these areas to maintain data integrity.
What This Does Not Solve
While deep merging can address many issues related to combining nested objects, it does not automatically solve all data integration challenges. Here are some limitations and considerations:
- Performance: Recursive merging can be computationally expensive for very large objects. Consider this when performance is a critical factor.
- Complex Data Structures: Deep merging assumes a standard object or array structure. More complex data objects or classes might require custom handling.
- Specific Business Logic: Some merging logic might need to reflect specific business rules that are not covered by generic strategies.
For developers working in environments like React or Node.js, where performance and state management are key, integrating deep merging techniques can significantly improve how smoothly your application runs. I recommend exploring how you can implement these strategies in your own work, perhaps alongside other performance optimization techniques as discussed in Building High-Performance Web Apps: Lessons from iGaming.
Next Steps
On Monday morning, you can start by identifying parts of your codebase where shallow merging is used and assess whether these could benefit from a deep merge approach. Test the provided deep merge utility on a small scale and gradually integrate it into your larger system.
If you're managing a JavaScript-heavy application, consider reading more about JavaScript class structures and other advanced topics in Building an IPv4 Subnet Calculator Using JavaScript Classes.
Ultimately, mastering deep merging can serve as a vital tool in your JavaScript toolkit, ensuring that your applications maintain data integrity and robustness as they scale.
Sources
Deep Merging in JavaScript: How to Recursively Combine Objects Without Losing Nested Properties
Every claim above was checked against this source before publishing. The analysis, the code and the opinions are mine.
Frequently asked
Why does the spread operator cause data loss in nested objects?
The spread operator performs a shallow copy, only duplicating top-level properties. Nested objects are referenced, leading to data loss.
What is a deep merge in JavaScript?
A deep merge involves recursively combining nested properties of objects, preserving all data and avoiding overwriting nested structures.
How can I implement a deep merge in TypeScript?
Use a recursive function that checks for object types and applies merging strategies for arrays and primitive values, ensuring type safety.
What are the limitations of deep merging?
Deep merging can be computationally expensive for large objects and might not address specific business logic or complex data structures.