Loading content...
The problems we identified in the previous page—expensive creation, unknown types, and complex configuration—share a common thread: external code lacks the knowledge to create copies correctly. The caller doesn't know the concrete type, doesn't have access to private fields, and shouldn't need to understand internal initialization logic.\n\nThe solution is elegantly simple: move the copying responsibility to the object itself. Each object provides a clone() method that returns a copy of itself. The caller simply invokes clone() without needing to know anything about the object's internals or concrete type.\n\nThis is the Prototype Pattern in its purest form: objects act as prototypes for creating new instances of the same kind.
By the end of this page, you will understand how to design and implement clone methods, grasp the interface contracts that make prototypes work polymorphically, and appreciate the design decisions that make cloning robust and maintainable.
The Prototype Pattern centers on a simple but powerful interface contract: any prototypeable object must declare a method that returns a copy of itself. While the specific method name varies by language convention (clone(), copy(), duplicate()), the semantics remain consistent.\n\nThe core contract:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
// The Prototype interface declares the cloning contract// Using generics ensures type-safe cloninginterface Prototype<T> { // Returns a copy of this object // The returned object has the same concrete type as 'this' clone(): T;} // Alternative: self-referential type for stricter typinginterface SelfCloneable { clone(): this;} // Example: A Shape prototype interfaceinterface ShapePrototype extends Prototype<ShapePrototype> { // Shape-specific methods draw(): void; area(): number;} // Concrete implementations return their own typeclass Circle implements ShapePrototype { constructor( public readonly x: number, public readonly y: number, public readonly radius: number, public readonly color: string ) {} // Circle.clone() returns a new Circle clone(): Circle { return new Circle(this.x, this.y, this.radius, this.color); } draw(): void { console.log(`Drawing ${this.color} circle at (${this.x}, ${this.y}) with radius ${this.radius}`); } area(): number { return Math.PI * this.radius * this.radius; }} class Rectangle implements ShapePrototype { constructor( public readonly x: number, public readonly y: number, public readonly width: number, public readonly height: number, public readonly color: string ) {} // Rectangle.clone() returns a new Rectangle clone(): Rectangle { return new Rectangle(this.x, this.y, this.width, this.height, this.color); } draw(): void { console.log(`Drawing ${this.color} rectangle at (${this.x}, ${this.y})`); } area(): number { return this.width * this.height; }} // Client code works with abstract ShapePrototype// No need to know concrete types!function duplicateShape(shape: ShapePrototype): ShapePrototype { return shape.clone(); // Polymorphic cloning} // Usageconst originalCircle = new Circle(10, 20, 5, "blue");const copiedCircle = duplicateShape(originalCircle); // Returns a CirclecopiedCircle.draw(); // "Drawing blue circle at (10, 20) with radius 5"Java's built-in Cloneable interface and Object.clone() method have well-documented design flaws. Use explicit clone or copy methods (as shown above) rather than relying on Object.clone(). Key issues: (1) Cloneable is a marker interface with no methods, (2) Object.clone() has unusual exception semantics, (3) The shallow-copy default is often wrong. Effective Java recommends copy constructors or factory methods instead.
A well-designed clone method must address several concerns: copying primitive values, handling object references, maintaining invariants, and respecting immutability. Let's dissect the structure of a robust clone implementation:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
interface Prototype<T> { clone(): T;} // A realistic example: a Document with multiple fields of different typesclass Document implements Prototype<Document> { // Primitive fields - can be copied directly private title: string; private version: number; private isPublished: boolean; // Immutable object - can be shared (no need to clone) private readonly createdAt: Date; // Mutable object - MUST be cloned for true copy private metadata: Map<string, string>; // Array of primitives - must create new array private tags: string[]; // Array of objects - each element must be cloned private sections: Section[]; // Nullable field - handle null case private reviewedBy: Author | null; constructor( title: string, metadata: Map<string, string>, tags: string[], sections: Section[] ) { this.title = title; this.version = 1; this.isPublished = false; this.createdAt = new Date(); // Set once, never changes this.metadata = metadata; this.tags = tags; this.sections = sections; this.reviewedBy = null; } // The clone method: structured approach clone(): Document { // Step 1: Create new instance // Note: We can't use the public constructor because it sets // createdAt to a new Date. We need a private copy mechanism. // Step 2: Copy primitives (direct assignment is fine) const copy = Object.create(Document.prototype); copy.title = this.title; copy.version = this.version; copy.isPublished = this.isPublished; // Step 3: Handle immutable objects (can share reference) // BUT: Date is technically mutable in JS! Clone it anyway. copy.createdAt = new Date(this.createdAt.getTime()); // Step 4: Clone mutable objects (must create new instances) copy.metadata = new Map(this.metadata); // Step 5: Clone arrays of primitives (spread creates new array) copy.tags = [...this.tags]; // Step 6: Clone arrays of objects (each element must be cloned) copy.sections = this.sections.map(section => section.clone()); // Step 7: Handle nullable references copy.reviewedBy = this.reviewedBy ? this.reviewedBy.clone() : null; return copy; }} // Section must also be cloneableclass Section implements Prototype<Section> { constructor( public title: string, public content: string, public order: number ) {} clone(): Section { // All primitives - simple constructor call return new Section(this.title, this.content, this.order); }} // Author must also be cloneable if references are clonedclass Author implements Prototype<Author> { constructor( public readonly id: string, public name: string, public email: string ) {} clone(): Author { return new Author(this.id, this.name, this.email); }}Many languages idiomatically implement cloning through a copy constructor—a constructor that takes an instance of the same class and initializes the new object from it. The clone() method then simply delegates to this copy constructor.\n\nThis pattern offers several advantages:\n- Constructor can enforce validation and invariants\n- Copy logic is centralized in one place\n- Works naturally with immutable objects\n- Familiar pattern for developers from C++ and other languages
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
interface Prototype<T> { clone(): T;} // Copy constructor pattern in TypeScriptclass GameCharacter implements Prototype<GameCharacter> { private readonly id: string; private name: string; private health: number; private maxHealth: number; private level: number; private position: { x: number; y: number }; private inventory: Item[]; private skills: Map<string, Skill>; // Primary constructor constructor(name: string, characterClass: CharacterClass) { this.id = generateUniqueId(); this.name = name; this.health = characterClass.baseHealth; this.maxHealth = characterClass.baseHealth; this.level = 1; this.position = { x: 0, y: 0 }; this.inventory = []; this.skills = new Map(characterClass.startingSkills); } // Copy constructor (private - only used internally) private static fromExisting(source: GameCharacter): GameCharacter { const character = Object.create(GameCharacter.prototype); // New instances get new unique IDs (game design decision) character.id = generateUniqueId(); // Copy all state from source character.name = `${source.name} (Copy)`; // Indicate it's a copy character.health = source.health; character.maxHealth = source.maxHealth; character.level = source.level; // Deep copy mutable objects character.position = { ...source.position }; character.inventory = source.inventory.map(item => item.clone()); character.skills = new Map( Array.from(source.skills.entries()) .map(([key, skill]) => [key, skill.clone()]) ); return character; } // Clone delegates to copy constructor clone(): GameCharacter { return GameCharacter.fromExisting(this); } // Methods... takeDamage(amount: number): void { this.health = Math.max(0, this.health - amount); } moveTo(x: number, y: number): void { this.position = { x, y }; }} class Item implements Prototype<Item> { constructor( public readonly itemId: string, public readonly name: string, public quantity: number ) {} clone(): Item { return new Item(this.itemId, this.name, this.quantity); }} class Skill implements Prototype<Skill> { constructor( public readonly skillId: string, public readonly name: string, public level: number, public cooldown: number ) {} clone(): Skill { return new Skill(this.skillId, this.name, this.level, this.cooldown); }} // Usage: Clone a character (e.g., for save/load or template-based creation)const warrior = new GameCharacter("Aragorn", CharacterClasses.WARRIOR);warrior.moveTo(100, 200);warrior.takeDamage(10); // Clone creates independent copy with new IDconst clonedWarrior = warrior.clone();console.log(warrior.id !== clonedWarrior.id); // true - different instancesconsole.log(warrior.position !== clonedWarrior.position); // true - different objectsKeep the copy constructor private (or package-private) and expose only the clone() method to external code. This ensures all cloning goes through a single, controllable point—you can add logging, validation, or transformation logic in clone() without changing the copy constructor.
The true power of the Prototype Pattern emerges when cloning operates polymorphically—when you can clone objects through a base type reference without knowing the concrete type. This is what eliminates the type-switching antipattern we discussed earlier.\n\nKey insight: When you call prototype.clone() through a base type reference, the concrete class's clone implementation executes. Each class handles its own copying, and the caller remains oblivious to the concrete type.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
// Prototype interface for shapesinterface Shape { clone(): Shape; draw(): void; area(): number; getInfo(): string;} // Concrete prototypes - each handles its own cloningclass Circle implements Shape { constructor( public x: number, public y: number, public radius: number, public color: string, public lineWidth: number ) {} clone(): Circle { return new Circle(this.x, this.y, this.radius, this.color, this.lineWidth); } draw(): void { console.log(`Drawing circle: ${this.getInfo()}`); } area(): number { return Math.PI * this.radius ** 2; } getInfo(): string { return `Circle(x=${this.x}, y=${this.y}, r=${this.radius}, color=${this.color})`; }} class Star implements Shape { constructor( public x: number, public y: number, public points: number, public outerRadius: number, public innerRadius: number, public color: string, public rotation: number ) {} clone(): Star { return new Star( this.x, this.y, this.points, this.outerRadius, this.innerRadius, this.color, this.rotation ); } draw(): void { console.log(`Drawing star: ${this.getInfo()}`); } area(): number { // Approximation for star area return 0.5 * this.points * this.outerRadius * this.innerRadius; } getInfo(): string { return `Star(x=${this.x}, y=${this.y}, points=${this.points})`; }} class Polygon implements Shape { constructor( public vertices: Array<{ x: number; y: number }>, public color: string, public fillColor: string | null ) {} clone(): Polygon { // Must deep-clone the vertices array const clonedVertices = this.vertices.map(v => ({ ...v })); return new Polygon(clonedVertices, this.color, this.fillColor); } draw(): void { console.log(`Drawing polygon: ${this.getInfo()}`); } area(): number { // Shoelace formula for polygon area let area = 0; const n = this.vertices.length; for (let i = 0; i < n; i++) { const j = (i + 1) % n; area += this.vertices[i].x * this.vertices[j].y; area -= this.vertices[j].x * this.vertices[i].y; } return Math.abs(area) / 2; } getInfo(): string { return `Polygon(vertices=${this.vertices.length}, color=${this.color})`; }} // ============================================// CLIENT CODE: Works with abstract Shape type// No knowledge of concrete types!// ============================================ class DrawingCanvas { private shapes: Shape[] = []; addShape(shape: Shape): void { this.shapes.push(shape); } // Duplicate selected shape - POLYMORPHIC CLONING duplicateShape(index: number): Shape | null { if (index < 0 || index >= this.shapes.length) return null; const original = this.shapes[index]; const copy = original.clone(); // Polymorphic! this.shapes.push(copy); return copy; } // Duplicate all shapes - works on heterogeneous collection duplicateAll(): void { const copies = this.shapes.map(shape => shape.clone()); this.shapes.push(...copies); } // Create stamp from shape - multiple copies at different positions createStamp(shape: Shape, positions: Array<{ x: number; y: number }>): void { for (const pos of positions) { const copy = shape.clone(); // Note: Would need a setPosition method or similar // to actually reposition. The point is cloning works. this.shapes.push(copy); } } draw(): void { this.shapes.forEach(shape => shape.draw()); }} // Usage demonstrationconst canvas = new DrawingCanvas(); canvas.addShape(new Circle(100, 100, 50, "blue", 2));canvas.addShape(new Star(200, 200, 5, 40, 20, "gold", 0));canvas.addShape(new Polygon([ { x: 300, y: 100 }, { x: 350, y: 150 }, { x: 325, y: 200 }, { x: 275, y: 200 }, { x: 250, y: 150 }], "green", "lightgreen")); // Duplicate heterogeneous collection - all shapes clone correctlycanvas.duplicateAll();canvas.draw();Polymorphic cloning relies on dynamic method dispatch—the runtime mechanism that calls the correct implementation based on the object's actual type, not the declared type of the reference. This is standard OOP behavior, but it's worth noting explicitly: clone() works polymorphically because it's a virtual method call.
When prototypes form an inheritance hierarchy, cloning becomes more nuanced. Subclasses must clone their own fields while also ensuring parent class fields are properly copied. Several patterns handle this reliably:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
// Pattern 1: Chain of Clone Calls (Copy Constructor Chaining)abstract class UIComponent { protected x: number; protected y: number; protected width: number; protected height: number; protected styleClasses: string[]; constructor(x: number, y: number, width: number, height: number) { this.x = x; this.y = y; this.width = width; this.height = height; this.styleClasses = []; } // Protected copy initialization for subclasses protected copyFrom(source: UIComponent): void { this.x = source.x; this.y = source.y; this.width = source.width; this.height = source.height; this.styleClasses = [...source.styleClasses]; } abstract clone(): UIComponent;} class Button extends UIComponent { private label: string; private onClick: (() => void) | null; private isDisabled: boolean; constructor(x: number, y: number, width: number, height: number, label: string) { super(x, y, width, height); this.label = label; this.onClick = null; this.isDisabled = false; } clone(): Button { const copy = new Button(this.x, this.y, this.width, this.height, this.label); copy.copyFrom(this); // Copy parent fields copy.label = this.label; copy.onClick = this.onClick; // Function reference shared copy.isDisabled = this.isDisabled; return copy; } setOnClick(handler: () => void): void { this.onClick = handler; } disable(): void { this.isDisabled = true; }} class IconButton extends Button { private icon: string; private iconPosition: "left" | "right"; constructor( x: number, y: number, width: number, height: number, label: string, icon: string ) { super(x, y, width, height, label); this.icon = icon; this.iconPosition = "left"; } clone(): IconButton { // Create instance - this chains constructors up the hierarchy const copy = new IconButton( this.x, this.y, this.width, this.height, // Note: We need access to parent's label field // This is where protected fields or copy methods help "", this.icon ); // The better approach: use Object.assign or explicit copy return Object.assign( Object.create(IconButton.prototype), this, { // Clone mutable fields styleClasses: [...this.styleClasses], } ); } setIconPosition(position: "left" | "right"): void { this.iconPosition = position; }} // ============================================// Pattern 2: Abstract Clone with Subclass Override// ============================================ abstract class Animal { protected name: string; protected age: number; protected traits: string[]; constructor(name: string, age: number) { this.name = name; this.age = age; this.traits = []; } // Factory method pattern for cloning abstract clone(): Animal; // Template method to copy base fields protected cloneBaseFieldsTo(target: Animal): void { target.name = this.name; target.age = this.age; target.traits = [...this.traits]; }} class Dog extends Animal { private breed: string; private isVaccinated: boolean; private favoriteToysIds: string[]; constructor(name: string, age: number, breed: string) { super(name, age); this.breed = breed; this.isVaccinated = false; this.favoriteToysIds = []; } clone(): Dog { const copy = new Dog(this.name, this.age, this.breed); this.cloneBaseFieldsTo(copy); copy.isVaccinated = this.isVaccinated; copy.favoriteToysIds = [...this.favoriteToysIds]; return copy; }} class GuideDog extends Dog { private certificationId: string | null; private assignedTo: string | null; private specializations: string[]; constructor(name: string, age: number, breed: string) { super(name, age, breed); this.certificationId = null; this.assignedTo = null; this.specializations = []; } clone(): GuideDog { // Start with superclass clone const dogClone = super.clone(); // Create GuideDog from Dog (manual field copy) const copy = Object.assign( Object.create(GuideDog.prototype), dogClone ) as GuideDog; // Copy GuideDog-specific fields copy.certificationId = this.certificationId; copy.assignedTo = this.assignedTo; copy.specializations = [...this.specializations]; return copy; } certify(certId: string): void { this.certificationId = certId; }}A subtle but important detail: clone methods typically use covariant return types—the return type matches the concrete class, not the base interface. This provides better type safety and eliminates unnecessary casts.\n\nCovariance means a subclass's override can return a more specific type than the parent's declaration. Most modern typed languages support this for method return types.
12345678910111213141516171819202122232425262728
// WITHOUT covariant returnsinterface Prototype { clone(): Prototype;} class Button implements Prototype { label: string; constructor(label: string) { this.label = label; } // Returns Prototype - loses type info clone(): Prototype { return new Button(this.label); }} // Client must cast!const original = new Button("Click");const copy = original.clone(); // ERROR: 'label' not on Prototype// console.log(copy.label); // Must cast to regain typeconst typed = copy as Button;console.log(typed.label); // "Click"123456789101112131415161718192021222324252627
// WITH covariant returnsinterface Prototype<T> { clone(): T;} class Button implements Prototype<Button> { label: string; constructor(label: string) { this.label = label; } // Returns Button - preserves type clone(): Button { return new Button(this.label); }} // Client gets correct type!const original = new Button("Click");const copy = original.clone(); // Works: copy is typed as Buttonconsole.log(copy.label); // "Click" // No cast neededcopy.label = "Updated";TypeScript supports this as a return type, which automatically refers to the concrete class. This provides true covariance without explicit generics: clone(): this. However, implementing this correctly in a hierarchy requires care—each subclass must return the proper concrete type.
Different languages and situations call for different cloning implementation strategies. Here are the most common approaches:
123456789101112131415161718192021222324252627282930
// Manual field-by-field copying// Most explicit, most control, most maintenance burdenclass Product { id: string; name: string; price: number; tags: string[]; metadata: Map<string, string>; clone(): Product { const copy = new Product(); copy.id = this.id; copy.name = this.name; copy.price = this.price; copy.tags = [...this.tags]; copy.metadata = new Map(this.metadata); return copy; }} // Pros:// - Full control over what gets copied// - Can transform during copy// - Can skip certain fields intentionally// - Clear and debuggable // Cons:// - Must update when adding new fields// - Easy to forget a field// - Verbose for many fieldsFor most production code, manual clone methods are recommended despite their verbosity. They're explicit, debuggable, and fully controlled. Use spread/assign for simple data objects without nested references. Use serialization only when you need an easy deep copy and can accept its limitations.
The clone method provides a powerful and flexible mechanism for object creation that sidesteps the limitations of constructor-based instantiation:
clone(): T method that returns a copy of the object.What's next:\n\nThe clone mechanisms we've discussed can create either shallow copies (sharing nested object references) or deep copies (independently copying everything). This distinction is critical for correctness. The next page explores shallow vs deep copy semantics in detail—when each is appropriate, how to implement them correctly, and the pitfalls to avoid.
You now understand the clone method as the core mechanism of the Prototype Pattern. Next, we'll dive into the crucial distinction between shallow and deep copying.