Published on

TypeScript: How to Copy One Map to Another Map

Table of Contents

When working with Map in TypeScript, you may come across a situation where you need to copy the contents of one Map to another Map. This can be useful for creating a new Map with the same key-value pairs as an existing Map, or for making a backup of a Map before making changes to it.

Copy via Constructor (Shallow)

Here's an example of how to copy one Map to another Map using a shallow copy:

ts-map-clone-shallow.tsx
interface Member {
  fullName: string
  subscriptionID?: number
}

const member1: Member = { fullName: 'John Smith', subscriptionID: 123 }
const member2: Member = { fullName: 'Mike Doe' }

const membersMap = new Map<number, Member>()
membersMap.set(1,member1)
membersMap.set(2,member2)

const membersMapCloneShallow = new Map<number, Member>(membersMap)

//All maps have same values
/* {
  "fullName": "John Smith",
  "subscriptionID": 123
}*/
 console.log(membersMap.get(1))
 console.log(membersMapCloneShallow.get(1))


//Change original object
membersMap.get(1)!.fullName = "mr jitter";
//Cloned object also afected as is by reference
console.log(membersMap.get(1))  //  "fullName": "mr jitter"
console.log(membersMapCloneShallow.get(1))  //  "fullName": "mr jitter"

Here, we're creating a Map called membersMap with Member objects as values. We're adding two key-value pairs to the Map using the set method. Then, we're creating a new Map called membersMapCloneShallow by passing membersMap as an argument to the Map constructor. This creates a shallow copy of membersMap in membersMapCloneShallow.

Note that when copying a Map, a shallow copy is created. ☹️

This means that the keys and values in the original Map are copied by reference, rather than by value. If the keys or values are mutable objects, changes made to them in one Map will be reflected in the other Map as well.

Copy via Iterator (Shallow and Deep)

Here's an example of how to copy one Map to another Map using iteration:

ts-map-clone-iteration.tsx
interface Member {
  fullName: string
  subscriptionID?: number
}

const member1: Member = { fullName: 'John Smith', subscriptionID: 123 }
const member2: Member = { fullName: 'Mike Doe' }

const membersMap = new Map<number, Member>()
membersMap.set(1,member1)
membersMap.set(2,member2)

let membersMapCloneShallow = new Map<number, Member>()
let membersMapCloneDeep = new Map<number, Member>()

membersMap.forEach((value, key, map) => {
  membersMapCloneShallow.set(key, value)
  membersMapCloneDeep.set(key, {...value})
})

//All maps have same values
/* {
  "fullName": "John Smith",
  "subscriptionID": 123
}*/
 console.log(membersMap.get(1))
 console.log(membersMapCloneShallow.get(1))
 console.log(membersMapCloneDeep.get(1))

//Change original object
membersMap.get(1)!.fullName = "mr jitter";
console.log(membersMap.get(1))  //  "fullName": "mr jitter"
console.log(membersMapCloneShallow.get(1))  //  "fullName": "mr jitter"
console.log(membersMapCloneDeep.get(1))  //  "fullName": "John Smith"

Here, with the spread operator we can achieve the deserialization and a new object is constructed to be referenced, and thus we have the deep copy.

Copy via Custom function (Deep)

In that case you'll need to implement a custom function that copies each key and value recursively.

ts-map-clone-deep-custom.tsx
const deepCopy = <K, V>(original: Map<K, V> | Set<V> | any): Map<K, V> | Set<V> | any => {
  if (original instanceof Map) {
    const copy = new Map();
    original.forEach((val, key) => {
      copy.set(deepCopy(key), deepCopy(val));
    })
    return copy
  } else if (original instanceof Set) {
    const copy = new Set<V>()
    original.forEach(val => {
      copy.add(deepCopy(val));
    })
    return copy
  } else if (Array.isArray(original)) {
    const copy = original.map(val => deepCopy(val))
    return copy
  } else if (original && typeof original === 'object') {
    const copy: any = {};
    Object.entries(original).forEach(([key, val]) => {
      copy[key] = deepCopy(val)
    });
    return copy
  } else {
    return original
  }
}

WARNING

Please notice here the forEach signature of iterating a Map vs an Array

Array: forEach(element, index, array)

Map : forEach(value, key, map)

ts-map-clone-deep-usage.tsx
interface Member {
  fullName: string
  subscriptionID?: number
  address?: Address
  contacts?: Map<string, string>
}

interface Address {
  street: string
  city: string
}

//Maps with nested Objects
const membersMap = new Map<number, Member>()

const johnAddress: Address = { street: 'Rue de l Eventail', city: 'Le Mans' };
const johnContacts = new Map<string, string>()
johnContacts.set('email', 'john.smith@domain.com')
johnContacts.set('phone', '12-1234-567890')

const john: Member = {
    fullName: 'John Smith',
    subscriptionID: 123 ,
    address: johnAddress ,
    contacts: johnContacts}

membersMap.set(1,john)

//Show contacts
membersMap.get(1)?.contacts?.forEach((value, key) => {
  console.log(`contact ${key}: ${value}`)
})

 const membersMapDeepClone = deepCopy(membersMap)

membersMap.get(1)!.address!.city = "Paris"
membersMap.get(1)!.address!.street = "Rue Desaix"

console.log(membersMap.get(1)) //  "address": {"street": "Rue Desaix","city": "Paris"}
console.log(membersMapDeepClone.get(1)) //  "address": {"street": "Rue de l Eventail","city": "Le Mans"}

Here, we're creating a Map membersMap with Member objects as keys and Map as values. We're adding one Nested Object key-value pair for John to the Map using the set method.

Then, we're creating a new Map membersMapDeepClone by passing membersMap to the deepCopy arrow function, which creates a deep copy of the original Map.

Copy via Deserialization OneLiner (Deep)

This approach of using JSON.parse(JSON.stringify()) to create a deep copy of a Map is a common technique used to clone objects in JavaScript. The way it works is: first serializes the object into a JSON string and then deserializes it into a new object. This process creates a new object with a new reference, thus making a deep copy of the original object.

However, this approach has some limitations, such as not being able to handle circular references or preserving the original object's prototype. In contrast, deepCopy that provided previously can handle those cases.

Regarding which approach is preferred, it depends on the specific use case and requirements. If your object does not have any circular references, and you don't need to preserve the original object's prototype, then the JSON.parse(JSON.stringify()) approach can be a simpler and faster solution.

However, if you need a more robust and customizable solution that can handle any object, including circular references, then the deepCopy function is a better option.

In general, it's a good practice to use a library or utility function, such as Lodash's cloneDeep, for cloning objects, as they have been tested and optimized for performance and reliability.

ts-map-clone-deep-liner.tsx
const membersMapDeepClone = new Map(JSON.parse(JSON.stringify(Array.from(membersMap))))
console.log(membersMapDeepClone.get(john)) // ✅ undefined / References disolved

Summary

we have seen :

  • Copy via Constructor (Shallow )
  • Copy via Iterator (Shallow and Deep)
  • Copy via Custom function (Deep)
  • Copy via Deserialization OneLiner (Deep)