Programming

The Prototype Pattern: Copy Objects Easily in TypeScript

Do you need to create many similar objects? The Prototype pattern helps you clone objects instead of building them from scratch. This saves time and resources.

Introduction

I’ve built software for 15 years. During this time, one pattern has saved me more trouble than almost any other. The Prototype Pattern is simple but powerful. Let me show you why it matters.

The Core Problem It Solves

The Prototype Pattern fixes one common issue: making many similar objects without wasting resources.

When you need dozens of objects with the same structure but different values, creating each from scratch wastes time and memory. You repeat the same setup code over and over.

The Prototype Pattern offers a better way to make one object, then copy it when you need more. Simple!!!

Key Benefits Using Prototype Pattern

  • Performance Copying objects is faster than creating them from scratch
  • Memory Efficiency Share common setup and initialization code
  • Flexibility Add new prototypes at runtime without changing client code
  • Simplicity Reduce complex inheritance hierarchies
  • Consistency Create objects with standard configurations

These benefits make the Prototype Pattern essential for modern applications.

How It Works in Plain Terms

  1. Create one prototype object that can copy itself
  2. When you need another object, clone the prototype
  3. Change only what needs to be different
  4. Repeat as needed

That’s it. No complex theory. Just practical code that works.

Clean Example

Let’s look at a basic implementation:

interface Prototype {
  clone(): Prototype;
}

class Document implements Prototype {
  constructor(
    public title: string,
    public content: string,
    private created: Date
  ) {}
  
  clone(): Document {
    return new Document(
      this.title,
      this.content,
      new Date(this.created)
    );
  }
}

The interface demands one thing: a clone() method. Our Document class fulfills this by making a new document with the same properties.

Here’s how to use it:

// Make your prototype
const template = new Document(
  "Monthly Report",
  "This report covers monthly metrics for...",
  new Date()
);

// Clone and modify
const janReport = template.clone();
janReport.title = "January Report";
janReport.content = "January saw a 15% increase in users...";

const febReport = template.clone();
febReport.title = "February Report";
febReport.content = "February metrics show 7% growth in...";

We only change what differs in each report. Everything else comes from the prototype.

Shallow vs. Deep Copying

This is where bugs often hide. Let me explain:

Shallow Copy only duplicates top-level properties. If those properties point to other objects, both copies share those objects. Changes to shared objects affect all copies.

Deep Copy duplicates everything, including nested objects. This makes each copy fully independent.

Here’s how to make a deep copy:

class Report implements Prototype {
  constructor(
    public title: string,
    public data: object[]
  ) {}
  
  clone(): Report {
    // Deep copy the data array and its objects
    const clonedData = this.data.map(item => ({...item}));
    
    return new Report(
      this.title,
      clonedData
    );
  }
}

This ensures changes to one report’s data won’t affect others.

A Real-World Implementation

I’ve used this shape editor pattern in multiple projects:

// Base interface
interface Shape extends Prototype {
  draw(): void;
  clone(): Shape;
}

// Concrete shapes
class Circle implements Shape {
  constructor(
    public x: number,
    public y: number,
    public radius: number,
    public color: string
  ) {}
  
  draw(): void {
    console.log(`Drawing a ${this.color} circle at (${this.x},${this.y}) with radius ${this.radius}`);
  }
  
  clone(): Circle {
    return new Circle(this.x, this.y, this.radius, this.color);
  }
}

class Rectangle implements Shape {
  constructor(
    public x: number,
    public y: number,
    public width: number,
    public height: number,
    public color: string
  ) {}
  
  draw(): void {
    console.log(`Drawing a ${this.color} rectangle at (${this.x},${this.y}) with size ${this.width}x${this.height}`);
  }
  
  clone(): Rectangle {
    return new Rectangle(this.x, this.y, this.width, this.height, this.color);
  }
}

// Storage for our prototypes
class ShapeFactory {
  private shapes: Map<string, Shape> = new Map();
  
  registerShape(name: string, shape: Shape): void {
    this.shapes.set(name, shape);
  }
  
  createShape(name: string): Shape {
    const shape = this.shapes.get(name);
    if (!shape) {
      throw new Error(`Shape "${name}" doesn't exist`);
    }
    
    return shape.clone();
  }
}

We register common shapes once and clone them as needed:

const factory = new ShapeFactory();

// Register standard shapes
factory.registerShape("smallRedCircle", new Circle(0, 0, 5, "red"));
factory.registerShape("banner", new Rectangle(0, 0, 100, 30, "yellow"));

// Create shapes from prototypes
const circle1 = factory.createShape("smallRedCircle");
circle1.x = 10;
circle1.y = 15;
circle1.draw();

const rect = factory.createShape("banner");
rect.color = "green";
rect.draw();

This approach has clear benefits:

  1. We define standard shapes in one place
  2. Creating variations is fast and clear
  3. Adding new shapes doesn’t break existing code
  4. Performance improves by reusing common values

Common Questions About the Prototype Pattern

When should I use this pattern?

Use the Prototype pattern when:

  • Creating objects is more expensive than copying them
  • You need many similar objects with only small differences
  • You want to hide the complexity of creating objects
  • You need to create objects at runtime that share a class structure

How is this different from the Factory pattern?

  • Factory Pattern Creates different types of objects based on input
  • Prototype Pattern Creates copies of existing objects
  • Together They work well when a factory uses prototypes to create objects

What are the limitations?

  • Complex objects with circular references are hard to clone
  • Deep copying can be tricky and performance-intensive
  • Some objects can’t be easily copied (like database connections)

Combining Prototype, Factory, and Singleton

The real magic happens when you combine design patterns. Here’s how the Prototype pattern works with Factory and Singleton patterns:

// Singleton registry for prototypes
class PrototypeRegistry {
  private static instance: PrototypeRegistry | null = null;
  private prototypes: Map<string, Prototype> = new Map();
  
  private constructor() {
    // Initialize with default prototypes
  }
  
  public static getInstance(): PrototypeRegistry {
    if (!PrototypeRegistry.instance) {
      PrototypeRegistry.instance = new PrototypeRegistry();
    }
    
    return PrototypeRegistry.instance;
  }
  
  register(key: string, prototype: Prototype): void {
    this.prototypes.set(key, prototype);
  }
  
  unregister(key: string): void {
    this.prototypes.delete(key);
  }
  
  getPrototype(key: string): Prototype | undefined {
    return this.prototypes.get(key)?.clone();
  }
}

// Factory that uses the prototype registry
class PrototypeBasedFactory {
  createShape(type: string): Shape | null {
    const registry = PrototypeRegistry.getInstance();
    const prototype = registry.getPrototype(type);
    
    if (prototype && prototype instanceof Shape) {
      return prototype;
    }
    
    return null;
  }
}

// Setup
const registry = PrototypeRegistry.getInstance();
registry.register("circle", new Circle(0, 0, 10, "black"));
registry.register("rectangle", new Rectangle(0, 0, 20, 10, "black"));

// Use
const factory = new PrototypeBasedFactory();
const myCircle = factory.createShape("circle");
if (myCircle) {
  myCircle.color = "purple";
  myCircle.draw();
}

This example combines:

  • Singleton Pattern Only one prototype registry exists
  • Prototype Pattern Objects know how to clone themselves
  • Factory Pattern Creates objects based on type requests

Conclusion

Think of the Prototype pattern as the DNA of your software objects. Just like DNA creates copies with small variations, the Prototype pattern lets you create object variations efficiently.

When combined with Factory and Singleton patterns, it handles even the most complex object creation needs.

Remember

good patterns solve real problems. The Prototype Pattern has proven its value time and again in real projects. Add it to your toolkit.

Related Articles

Back to top button