Deep clone an Object and preserve its type with TypeScript

Deep clone an Object and preserve its type with TypeScript

Sunny Sun lol

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 cloning 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.

The 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.

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

  • 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 the source Object will be missing after copying.

    
      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, and 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 to the above code is “Object.create”, it is equivalent to the 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 property in JavaScript not only has a value, but also has three attributes (configurable, enumerable, and writable). All four attributes are called property descriptors .

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

The complete function is listed here


        export class cloneable {
        public static deepCopy(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 cloning 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.

If you like this article, you may also like my other recent articles.
Use Cases For TypeScript Discriminated Union Types and Generics
One reason I really like Typescript is its Type system, it is practical and feature rich. Applying the types in right…javascript.plainenglish.io

Happy Programming!