Question

How to Clone Class Objects in TypeScript Without Constructor Errors

typescriptjavascript

Question

I have a base class called Entity that is extended by many subclasses such as Customer, Product, and ProductCategory.

I want to dynamically clone an object graph in TypeScript. For example, a Customer contains multiple Product objects, and each Product may contain a ProductCategory.

Here is a simplified example:

const cust: Customer = new Customer();

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

To clone the whole tree of objects, I created a clone() method in Entity:

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

When TypeScript is transpiled, it raises this error:

error TS2351: Cannot use 'new' with an expression whose type lacks a call or construct signature.

The script still works at runtime, but I would like to remove the TypeScript error and use a safer cloning approach.

Short Answer

By the end of this page, you will understand why new this.constructor() causes a TypeScript error, how cloning class instances differs from copying plain objects, and how to implement safer shallow and deep clone patterns for class-based models in TypeScript.

Concept

In TypeScript, cloning an object is not always as simple as copying its properties. The main reason is that class instances are more than plain objects.

A class instance can contain:

  • Properties
  • Methods inherited from the prototype
  • Nested class instances
  • Arrays of class instances
  • Special initialization logic in the constructor

The error in your example happens because TypeScript does not know that this.constructor is definitely a constructable class type. At runtime, JavaScript allows it, but TypeScript's type system requires stronger guarantees.

There are really two separate topics here:

  1. How to create a new instance of the same class
  2. How to copy nested objects correctly

Your current code tries to solve both at once, but it has a few issues:

  • this.constructor is not typed as a class constructor
  • this.clone() inside the loop clones the whole object repeatedly instead of cloning the current property
  • typeof value === "object" is too broad because it includes arrays and null
  • Deep cloning class instances requires care if you want to preserve the correct class types

Why this matters in real programming:

  • You may want to duplicate data before editing it in a form
  • You may need immutable updates in state management
  • You may need to create copies of domain models without sharing references
  • You may want to avoid accidental mutation of nested data

The key idea is: copying object graphs safely requires both type awareness and a clear cloning strategy.

Mental Model

Think of a class instance like a house blueprint plus furniture.

  • The class is the blueprint
  • The instance properties are the furniture inside the house
  • A shallow copy builds a new house but reuses the same furniture references
  • A deep copy builds a new house and also duplicates the furniture inside it

Now imagine saying: "Build me another house using whatever blueprint this house came from." That is what new this.constructor() is trying to do.

JavaScript says, "Fine, I can try." TypeScript says, "Prove to me that this blueprint is actually constructable."

That is why the code runs in JavaScript but causes a TypeScript error.

Take Quiz

Syntax and Examples

A common beginner-friendly solution is to separate instance creation from property copying.

Example 1: Shallow clone with Object.assign

class Entity {
  clone<T extends this>(): T {
    return Object.assign(
      Object.create(Object.getPrototypeOf(this)),
      this
    );
  }
}

What this does

  • Object.getPrototypeOf(this) keeps the same prototype
  • Object.create(...) creates a new object of the same runtime type
  • Object.assign(...) copies properties onto it

This is a shallow clone.

If a Customer has a products array, both the original and cloned customer will still reference the same array.

Example 2: Simple deep clone for nested entities

class Entity {
  clone<T extends this>(): T {
    const cloneObj = Object.create(Object.getPrototypeOf(this));

    for (const key in this) {
      const value = this[key as keyof this];
      cloneObj[key] = this.cloneValue(value);
    }

    return cloneObj;
  }

  private cloneValue(value: any): any {
    if (value === null || value === undefined) {
      return value;
    }

    if (Array.isArray(value)) {
      return value.map(item => this.cloneValue(item));
    }

    if (value instanceof Entity) {
      return value.clone();
    }

    if (typeof value === "object") {
      return { ...value };
    }

    return value;
  }
}

Why this is better

  • It avoids new this.constructor()
  • It handles arrays separately
  • It clones nested Entity objects recursively
  • It does not accidentally call this.clone() for every property

Example 3: Using it with subclasses

class ProductCategory extends Entity {
  constructor(public name: string) {
    super();
  }
}

class Product extends Entity {
  category?: ProductCategory;

  constructor(public id: number) {
    super();
  }
}

class Customer extends Entity {
  name = "";
  products: Product[] = [];
}

const customer = new Customer();
customer.name = "Alice";

const p1 = new Product(1);
p1.category = new ProductCategory("Books");
customer.products.push(p1);

const copy = customer.clone();

This produces a new Customer instance with cloned nested Product and ProductCategory instances.

Step by Step Execution

Consider this code:

class Entity {
  clone<T extends this>(): T {
    const cloneObj = Object.create(Object.getPrototypeOf(this));

    for (const key in this) {
      const value = this[key as keyof this];
      cloneObj[key] = this.cloneValue(value);
    }

    return cloneObj;
  }

  private cloneValue(value: any): any {
    if (value === null || value === undefined) return value;
    if (Array.isArray(value)) return value.map(item => this.cloneValue(item));
    if (value instanceof Entity) return value.clone();
    if (typeof value === "object") return { ...value };
    return value;
  }
}

Now suppose we run:

const customer = new Customer();
customer.name = "Alice";
customer.products.push(new Product(1));

const cloned = customer.clone();

Step by step

  1. customer.clone() is called.
  2. Object.getPrototypeOf(this) gets the prototype of customer, which is Customer.prototype.
  3. Object.create(...) creates a new empty object with the same prototype.
  4. The for...in loop goes through enumerable properties on customer.
  5. For name, the value is a string, so cloneValue() returns the same string.
  6. For products, the value is an array, so cloneValue() maps over the array.
  7. Each Product in the array is an Entity, so product.clone() is called recursively.
  8. Each nested object is copied into the new structure.
  9. The result is a new Customer instance with its own products array and cloned Product instances.

Important detail

Primitive values such as strings, numbers, and booleans are copied by value. Objects and arrays must be copied explicitly, or the clone will share references with the original.

Real World Use Cases

Cloning class objects is useful in many common situations.

Form editing

You load a Customer from the server and let a user edit it in a form. To avoid changing the original object until the user clicks Save, you clone it first.

Undo or cancel changes

Applications often keep an editable copy and restore the original if the user cancels.

State management

In front-end apps, direct mutation can cause bugs. Cloning helps create new state objects safely.

Data transformation

You may clone an API model before enriching it with calculated fields or temporary UI-specific fields.

Testing

Tests often clone sample data so each test starts with a clean independent object graph.

Take Quiz

Real Codebase Usage

In real projects, developers usually avoid a single magical clone method for every case unless the object model is simple and well controlled.

Common patterns include:

Explicit clone methods per class

class Product extends Entity {
  constructor(public id: number, public name: string) {
    super();
  }

  clone(): Product {
    return new Product(this.id, this.name);
  }
}

This is the safest option when constructors matter.

Factory methods

Instead of cloning through constructors directly, teams use methods like:

  • fromJson()
  • toJson()
  • copy()
  • clone()

These make object creation predictable.

Guard clauses during cloning

Developers usually check for:

  • null
  • arrays
  • dates
  • nested entities
  • optional properties

Validation before copying

Some codebases validate the object shape before cloning, especially when data comes from APIs.

Prefer plain data when possible

In many apps, data models are kept as plain objects rather than rich class instances. This makes copying easier with spread syntax or dedicated utilities.

Avoid cloning everything blindly

A real object graph may contain:

  • circular references
  • functions
  • DOM objects
  • class instances with private state

That is why production code often uses targeted cloning rather than generic recursion.

Common Mistakes

1. Cloning the whole object instead of the current property

Your original code does this:

if (typeof this[attribute] === "object") {
  cloneObj[attribute] = this.clone();
}

This clones this again, not this[attribute].

Why it is a problem

  • It can create incorrect recursive structures
  • It may lead to infinite recursion

2. Using typeof value === "object" without checking for null

if (typeof value === "object") {
  // this also matches null
}

Fix

if (value !== null && typeof value === "object") {
  // safe object check
}

3. Forgetting that arrays are objects

if (typeof value === "object") {
  cloneObj[key] = { ...value }; // wrong for arrays
}

Fix

Handle arrays first:

if (Array.isArray(value)) {
  return value.map(item => this.cloneValue(item));
}

4. Expecting JSON cloning to preserve classes

const copy = JSON.parse(JSON.stringify(customer));

This creates a plain object, not a Customer instance.

Problems

  • methods are lost
  • prototypes are lost
  • dates become strings

5. Assuming constructors always take no arguments

const cloneObj = new this.constructor();

If a subclass constructor requires parameters, this may fail or create invalid objects.

6. Ignoring circular references

If A references B and B references A, naive recursion will crash with a stack overflow.

Avoidance

Use a Map of already-cloned objects for advanced deep cloning scenarios.

Comparisons

ApproachKeeps class prototype?Deep clone?Handles nested class instances?Notes
Spread syntax { ...obj }No for class behavior in practiceNoNoGood for plain objects
Object.assign into new instance/prototypeYesNoNoGood for shallow class copy
JSON.parse(JSON.stringify(obj))NoYes, for plain JSON dataNoLoses methods, dates, prototypes
Custom clone() methodYesCan beCan beBest when you control the classes
structuredClone()No class methods/prototype preservationYes for supported dataNot as class instancesGood for plain structured data

Shallow clone vs deep clone

TypeWhat gets copied?Shared nested references?Use when
Shallow cloneTop-level properties onlyYesNested data is safe to share
Deep cloneEntire object graphNoNested data must be independent

new this.constructor() vs Object.create(Object.getPrototypeOf(this))

TechniqueTypeScript-friendly?Constructor runs?Best for
new this.constructor()Usually no without typing workYesRare cases with properly typed constructors
Object.create(Object.getPrototypeOf(this))YesNoPrototype-preserving copies without constructor issues
Take Quiz

Cheat Sheet

Quick rules

  • Use a shallow clone when only top-level properties need copying.
  • Use a deep clone when nested objects or arrays must be independent.
  • Avoid new this.constructor() unless you explicitly type the constructor.
  • typeof value === "object" includes arrays and null.
  • JSON cloning does not preserve class instances.

Safe shallow clone for class instances

clone<T extends this>(): T {
  return Object.assign(
    Object.create(Object.getPrototypeOf(this)),
    this
  );
}

Basic deep clone pattern

private cloneValue(value: any): any {
  if (value === null || value === undefined) return value;
  if (Array.isArray(value)) return value.map(item => this.cloneValue(item));
  if (value instanceof Entity) return value.clone();
  if (typeof value === "object") return { ...value };
  return value;
}

Warning signs

  • Constructor requires parameters
  • Object contains circular references
  • Object contains Date, Map, Set, or functions
  • Object is not a plain object or controlled class instance

Best practice

If each class has special rules, write an explicit clone() method for that class.

FAQ

Why does new this.constructor() cause a TypeScript error?

Because TypeScript does not know that this.constructor is definitely a constructable type with a valid new signature.

Why does the code still work in JavaScript?

JavaScript checks this at runtime, not compile time. TypeScript is warning that the type is not safe enough.

Is Object.assign enough for cloning?

Only for shallow cloning. Nested arrays and objects will still be shared.

Can I use JSON.parse(JSON.stringify(obj)) to deep clone?

Only for simple JSON-like data. It removes class prototypes, methods, and special object types.

How do I clone nested class instances?

Use a custom recursive clone() method and handle arrays, nested entities, and plain objects separately.

Should every class have its own clone() method?

Often yes. It is usually the safest and clearest solution when constructors or nested fields differ.

What if my objects contain circular references?

A naive recursive clone will fail. You need a more advanced approach that tracks already-cloned objects with a Map.

Take Quiz

Mini Project

Description

Build a small TypeScript model system with Entity, Customer, Product, and ProductCategory, then implement cloning so editing a copied customer does not affect the original. This demonstrates prototype-preserving object cloning and recursive copying of nested entities.

Goal

Create a deep clone of a Customer instance so nested products and categories can be modified independently from the original object.

Requirements

  • Create an Entity base class with a reusable clone() method.
  • Add Customer, Product, and ProductCategory classes that extend Entity.
  • Ensure arrays of nested entities are cloned deeply.
  • Show that changing the clone does not change the original.
  • Keep the solution valid TypeScript without using new this.constructor().
Take Quiz

Keep learning

Related questions

View more