While building a configuration management utility for a client project, I had to merge objects in TypeScript. In that case, I just needed to combine two objects, but it became more complex when I had to deal with deeply nested properties, overlapping keys, and type safety.
While searching for a solution, I found several ways to merge objects in TypeScript. Using the spread operator for quick merges, to writing custom deep merge functions for recursive structures.
In this tutorial, I will explain all possible ways to merge objects in TypeScript using different methods, illustrated in the examples below.
Understanding Object Merging in TypeScript
When developing applications, you’ll often need to combine multiple objects. Perhaps you’re merging user settings with default configurations, or combining data from multiple API responses.
In TypeScript, we have several methods to achieve this, each with different type-safety guarantees and behavior.
Method 1: Using the Spread Operator
The spread operator (…) is my go-to method for merging objects. It’s simple, readable, and works great for most scenarios.
function mergeWithSpread<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
// Example
const userDefaults = { theme: 'dark', notifications: true };
const userPreferences = { notifications: false, fontSize: 'large' };
const mergedSettings = mergeWithSpread(userDefaults, userPreferences);
console.log(mergedSettings);
// Output: { theme: 'dark', notifications: false, fontSize: 'large' }Output:

Notice how the notifications property from userPreferences overwrites the one from userDefaults. This is because properties from the second object will override those from the first one if they have the same key.
Check out: Check If Object Is Undefined in TypeScript
Method 2: Using Object.assign()
Object.assign() is another common method that works similarly to the spread operator.
function mergeWithAssign<T, U>(target: T, source: U): T & U {
return Object.assign({}, target, source);
}
// Example
const californiaAddress = { state: 'CA', country: 'USA' };
const addressDetails = { street: '123 Main St', city: 'San Francisco' };
const fullAddress = mergeWithAssign(californiaAddress, addressDetails);
console.log(fullAddress);
// Output: { state: 'CA', country: 'USA', street: '123 Main St', city: 'San Francisco' }Output:

I always create a new empty object as the first parameter to avoid modifying the original objects.
Check out: Conditionally Add Property to Object in TypeScript
Method 3: Deep Merging with a Custom Function
The previous methods perform shallow merges. If your objects contain nested objects, you might need a deep merge.
function deepMerge(target: Record<string, any>, source: Record<string, any>): Record<string, any> {
const result: Record<string, any> = { ...target };
for (const key in source) {
if (
source[key] instanceof Object &&
key in target &&
target[key] instanceof Object
) {
result[key] = deepMerge(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
// Example
const companyDefaults = {
name: 'TechCorp',
contact: { email: 'info@example.com', phone: '555-1234' }
};
const updates = {
website: 'www.example.com',
contact: { email: 'support@example.com' }
};
const merged = deepMerge(companyDefaults, updates);
console.log(merged);
/* Output:
{
name: 'TechCorp',
contact: { email: 'support@example.com', phone: '555-1234' },
website: 'www.example.com'
}Output:

This function recursively merges nested objects, preserving properties that exist in the target but not in the source.
Check out: How to Check if an Object is Empty in TypeScript
Method 4: Using Libraries Like lodash
For complex merging needs, I often turn to libraries like lodash, which provide robust solutions.
import { merge } from 'lodash';
// Example
const productBase = {
id: 'P123',
specs: { color: 'red', size: 'medium' },
tags: ['clothing']
};
const productUpdate = {
name: 'T-Shirt',
specs: { material: 'cotton' },
tags: ['summer']
};
const fullProduct = merge({}, productBase, productUpdate);
console.log(fullProduct);
/* Output:
{
id: 'P123',
specs: { color: 'red', size: 'medium', material: 'cotton' },
tags: ['summer'],
name: 'T-Shirt'
}
*/Output:

Lodash’s merge function performs a deep merge and has special handling for arrays (note that arrays are replaced by default rather than merged).
Method 5: Using TypeScript Utility Types
TypeScript provides utility types that can help with type-safe object merging.
type Merge<T, U> = Omit<T, keyof U> & U;
// Example usage
interface UserBase {
id: number;
name: string;
email: string;
}
interface UserUpdate {
name: string;
address: string;
}
// Type for the merged object
type MergedUser = Merge<UserBase, UserUpdate>;
function mergeUser<T extends UserBase, U extends Partial<UserBase>>(base: T, update: U): Merge<T, U> {
return { ...base, ...update } as Merge<T, U>;
}
const user = mergeUser(
{ id: 123, name: 'John Doe', email: 'john@example.com' },
{ name: 'John Smith', address: '456 Park Ave, NYC' }
);
console.log(user);
// Output: { id: 123, name: 'John Smith', email: 'john@example.com', address: '456 Park Ave, NYC' }Output:

This approach focuses on the type-level merging, ensuring your merged objects maintain proper typing.
Check out: Implement Queues in TypeScript
Handling Arrays When Merging Objects
When merging objects with array properties, the default behavior of both spread and Object.assign() is to replace arrays rather than merge them.
// Example with arrays
const baseConfig = {
features: ['dark-mode', 'auto-save'],
version: '1.0'
};
const newConfig = {
features: ['voice-commands'],
api: 'v2'
};
// The 'features' array gets replaced, not merged
const config = { ...baseConfig, ...newConfig };
console.log(config);
// Output: { features: ['voice-commands'], version: '1.0', api: 'v2' }
// If you want to merge arrays instead:
const mergedConfig = {
...baseConfig,
...newConfig,
features: [...baseConfig.features, ...newConfig.features]
};
console.log(mergedConfig);
// Output: { features: ['dark-mode', 'auto-save', 'voice-commands'], version: '1.0', api: 'v2' }Output:

I’ve found that explicitly handling arrays is often necessary when merging configurations or user preferences.
Check out: Check if an Object is a String in TypeScript
Performance Considerations
From my experience, the spread operator and Object.assign() have similar performance for small objects. For large objects or when merging many objects, there can be performance differences:
- Spread operator typically creates a new object and is more readable
- Object.assign() can be slightly faster when merging many objects
- Deep merging functions can be significantly slower for complex nested objects
- Lodash’s merge is well-optimized, but adds a dependency
Conclusion
In this tutorial, we learned how to merge objects in TypeScript using the spread operator, Object.assign(), and deep merge functions.
For the nested structures, we explored how to perform deep merges using a custom recursive function. In deep merging, we used Lodash’s merge() method that handles deep merging.
Along with this, we also learned how to handle arrays during merging. These techniques will help you manage object combinations effectively in real-world TypeScript projects.

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.