Search code examples
javascripttypescript

typescript - cloning object


I have a super class that is the parent (Entity) for many subclass (Customer, Product, ProductCategory...)

I'm looking to clone dynamically an object that contains different sub objects in Typescript.

In example : a Customer that has different Product who has a ProductCategory

var cust:Customer  = new Customer ();

cust.name = "someName";
cust.products.push(new Product(someId1));
cust.products.push(new Product(someId2));

In order to clone the whole tree of object I created a function in Entity

public clone():any {
    var cloneObj = new this.constructor();
    for (var attribut in this) {
        if(typeof this[attribut] === "object"){
           cloneObj[attribut] = this.clone();
        } else {
           cloneObj[attribut] = this[attribut];
        }
    }
    return cloneObj;
}

The new rises the following error when it is transpiled to javascript: error TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature.

Although the script works, I would like to get rid of the transpiled error


Solution

  • Solving The Specific Issue

    You can use a type assertion to tell the compiler that you know better:

    public clone(): any {
        var cloneObj = new (this.constructor() as any);
        for (var attribut in this) {
            if (typeof this[attribut] === "object") {
                cloneObj[attribut] = this[attribut].clone();
            } else {
                cloneObj[attribut] = this[attribut];
            }
        }
        return cloneObj;
    }
    

    Cloning

    As of 2022, there is a proposal to allow structuredClone to deep copy many types.

    const copy = structuredClone(value)
    

    There are some limitations on what kind of thing you can use this on.

    Bear in mind that sometimes it is better to write your own mapping - rather than being totally dynamic. However, there are a few "cloning" tricks you can use that give you different effects.

    I will use the following code for all the subsequent examples:

    class Example {
      constructor(public type: string) {
    
      }
    }
    
    class Customer {
      constructor(public name: string, public example: Example) {
    
      }
    
      greet() {
        return 'Hello ' + this.name;
      }
    }
    
    var customer = new Customer('David', new Example('DavidType'));
    

    Option 1: Spread

    Properties: Yes
    Methods: No
    Deep Copy: No

    var clone = { ...customer };
    
    alert(clone.name + ' ' + clone.example.type); // David DavidType
    //alert(clone.greet()); // Not OK
    
    clone.name = 'Steve';
    clone.example.type = 'SteveType';
    
    alert(customer.name + ' ' + customer.example.type); // David SteveType
    

    Option 2: Object.assign

    Properties: Yes
    Methods: No
    Deep Copy: No

    var clone = Object.assign({}, customer);
    
    alert(clone.name + ' ' + clone.example.type); // David DavidType
    alert(clone.greet()); // Not OK, although compiler won't spot it
    
    clone.name = 'Steve';
    clone.example.type = 'SteveType';
    
    alert(customer.name + ' ' + customer.example.type); // David SteveType
    

    Option 3: Object.create

    Properties: Inherited
    Methods: Inherited
    Deep Copy: Shallow Inherited (deep changes affect both original and clone)

    var clone = Object.create(customer);
        
    alert(clone.name + ' ' + clone.example.type); // David DavidType
    alert(clone.greet()); // OK
    
    customer.name = 'Misha';
    customer.example = new Example("MishaType");
    
    // clone sees changes to original 
    alert(clone.name + ' ' + clone.example.type); // Misha MishaType
    
    clone.name = 'Steve';
    clone.example.type = 'SteveType';
    
    // original sees changes to clone
    alert(customer.name + ' ' + customer.example.type); // Misha SteveType
    

    Option 4: Deep Copy Function

    Properties: Yes
    Methods: No
    Deep Copy: Yes

    function deepCopy(obj) {
        var copy;
    
        // Handle the 3 simple types, and null or undefined
        if (null == obj || "object" != typeof obj) return obj;
    
        // Handle Date
        if (obj instanceof Date) {
            copy = new Date();
            copy.setTime(obj.getTime());
            return copy;
        }
    
        // Handle Array
        if (obj instanceof Array) {
            copy = [];
            for (var i = 0, len = obj.length; i < len; i++) {
                copy[i] = deepCopy(obj[i]);
            }
            return copy;
        }
    
        // Handle Object
        if (obj instanceof Object) {
            copy = {};
            for (var attr in obj) {
                if (obj.hasOwnProperty(attr)) copy[attr] = deepCopy(obj[attr]);
            }
            return copy;
        }
    
        throw new Error("Unable to copy obj! Its type isn't supported.");
    }
    
    var clone = deepCopy(customer) as Customer;
    
    alert(clone.name + ' ' + clone.example.type); // David DavidType
    // alert(clone.greet()); // Not OK - not really a customer
    
    clone.name = 'Steve';
    clone.example.type = 'SteveType';
    
    alert(customer.name + ' ' + customer.example.type); // David DavidType