circuit

Deep clone an Object and preserve its type with TypeScript

From Shallow Copy to Deep Clone with Type




Everything in the JavaScript world is an Object. We often need to clone an Object. When working with TypeScript, preserving the object type may also be required.

This article will explore the options of deep clone an Object with TypeScript. The implementations of the clone are not dependent on external libraries.

Shallow copy

A shallow copy using Object.Assign or Spread operator will duplicate the top-level properties. But the properties as an Object are copied as a reference after shallow copy, thus it is shared between the original source and target(copied Object).

const objShallowCopy = Object.assign({}, Obj1);
// or
const objShallowCopy = {...Obj1};

The above methods can not deep clone a complex Object properly. But it is good enough for cases when nested object properties are not required.

Simplest way to do a Deep copy

Using JSON.parse and JSON.stringify is the simplest way to deep clone an Object. With the one line code below, the nested properties of a complex object can be deep cloned.

const objCloneByJsonStringfy = JSON.parse(JSON.stringify(Obj1));

But it does have a few caveats.

  • It is slow due to the nature of the method involving serialization and deserialization of an Object to and from JSON. When cloning a large object using this method, performance will be a concern.

  • Dates type is not supported. Dates will be parsed as Strings, thus the Dates object in source object will be lost after copy.

  • It does not preserve the type of the object as well as the methods. As the code snippet below shows, the instanceof returns false, because it can not find the constructor in the Object’s prototype chain. And the functions within source Object will be missing after copy.

    const objCloneByJsonStringfy = JSON.parse(JSON.stringify(obj1)); // the type of obj1 is ObjectWithName console.log(objCloneByJsonStringfy instanceof ObjectWithName); // the output is false

Preserve the Type

To solve the issues above, the following recursive deep clone function is developed. It supports Date data type, keeps the original object class constructor and methods in its prototype chain. It is also compact and efficient.

The gist of the code is below. A new object is instantiated with the source object prototype, and reduce operator is used to recursively copy each property over.

return Array.isArray(source)
  ? source.map(item => deepCopy(item))
    : source instanceof Date
    ? new Date(source.getTime())
    : source && typeof source === 'object'
    ? Object.getOwnPropertyNames(source).reduce((o, prop) =>
      o[prop] = deepCopy(source[prop]);
      return o;
      }, Object.create(Object.getPrototypeOf(source))
      : source as T;

The key of the above code is “Object.create”, it is equivalent with following:

Object.create = function (o) {
    function F() {}
    F.prototype = o;
    return new F();
};

So the after-copy object will point to the same prototype of the source object.

Property descriptor

Each JavaScript property has not only value, but also has three more attributes (configurable, enumerable and writable). All the four attributes are called property descriptor.

To complete the deep clone function above and make it to be “true” copy of the original object, the property descriptor should be cloned as well as the value of property. We can use “Object.defineProperty” to achieve that.

The complete function is listed here

export class cloneable {
  public static deepCopy<T>(source: T): T {
    return Array.isArray(source)
    ? source.map(item => this.deepCopy(item))
    : source instanceof Date
    ? new Date(source.getTime())
    : source && typeof source === 'object'
          ? Object.getOwnPropertyNames(source).reduce((o, prop) => {
             Object.defineProperty(o, prop, Object.getOwnPropertyDescriptor(source, prop)!);
             o[prop] = this.deepCopy((source as { [key: string]: any })[prop]);
             return o;
          }, Object.create(Object.getPrototypeOf(source)))
    : source as T;
  }
}

Summary

In this article, We discuss the usage and pitfalls of JSON.Parse/JSON.Stringify in deep clone an Object. A custom solution is presented to achieve true deep cloning and preserve the type of an Object.

Hopefully this article can help you in copying objects with TypeScript.




Continue Learning