Merge Objects in TypeScript

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:

Merge Objects in TypeScript

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:

Merge two Objects in TypeScript

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:

Deep Merge Objects in TypeScript

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:

Merge Objects in TypeScript using Lodash

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:

Merge Objects in TypeScript using Utility Types

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:

Merge Array Objects in TypeScript

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.

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.