Prototype Pattern
Create new objects by copying an existing object (the prototype) rather than creating from scratch.
The Problemβ
You have a complex configuration object. You need variations of it, but constructing from scratch is tedious and error-prone.
The naive approach:
const baseConfig = {
server: { host: "localhost", port: 3000, ssl: { enabled: true, cert: "..." } },
database: { host: "localhost", pool: { min: 5, max: 20 } },
logging: { level: "info", format: "json" },
};
// Creating a test config - manual copying is painful
const testConfig = {
server: { ...baseConfig.server, port: 3001 },
database: { ...baseConfig.database, pool: { ...baseConfig.database.pool } },
logging: { ...baseConfig.logging, level: "debug" },
};
// Forgot to deep-copy ssl? Now they share the same object!
Shallow spread doesn't deep-clone. Nested objects are shared. Mutations leak.
The Solutionβ
Clone the prototype, then modify:
import { deepClone } from "@pithos/core/arkhe";
const baseConfig = {
server: { host: "localhost", port: 3000, ssl: { enabled: true, cert: "..." } },
database: { host: "localhost", pool: { min: 5, max: 20 } },
logging: { level: "info", format: "json" },
};
// Deep clone, then modify safely
const testConfig = deepClone(baseConfig);
testConfig.server.port = 3001;
testConfig.logging.level = "debug";
// baseConfig is untouched - no shared references
True deep copy. Modify freely without affecting the original.
Live Demoβ
Clone a config, edit fields in the clone, see the diff. Toggle between "Shallow Copy" and "Deep Clone" β shallow copy leaks mutations to the original on nested objects. Deep clone keeps them isolated.
- clone.ts
- Usage
/**
* Prototype pattern: deep clone vs shallow copy.
*
* deepClone from Arkhe creates a true deep copy.
* Spread only copies the top level β nested objects are shared references.
*/
import { deepClone } from "@pithos/core/eidos/prototype/prototype";
import type { AppConfig, ConfigPath, CloneMode, RefCheck } from "./types";
export function cloneConfig(config: AppConfig, mode: CloneMode): AppConfig {
if (mode === "deep") return deepClone(config);
return { ...config };
}
export function getField(config: AppConfig, path: ConfigPath): string | number | boolean {
switch (path) {
case "server.port": return config.server.port;
case "server.ssl.enabled": return config.server.ssl.enabled;
case "database.pool.max": return config.database.pool.max;
case "logging.level": return config.logging.level;
}
}
export function setField(config: AppConfig, path: ConfigPath, value: string | number | boolean): void {
switch (path) {
case "server.port": config.server.port = value as number; break;
case "server.ssl.enabled": config.server.ssl.enabled = value as boolean; break;
case "database.pool.max": config.database.pool.max = value as number; break;
case "logging.level": config.logging.level = value as string; break;
}
}
export function checkReferences(original: AppConfig, clone: AppConfig): RefCheck[] {
return [
{ label: "server", shared: original.server === clone.server },
{ label: "server.ssl", shared: original.server.ssl === clone.server.ssl },
{ label: "database", shared: original.database === clone.database },
{ label: "database.pool", shared: original.database.pool === clone.database.pool },
{ label: "logging", shared: original.logging === clone.logging },
];
}
import { useState } from "react";
import { RotateCcw, ChevronRight, ChevronLeft, Check } from "lucide-react";
import { useConfigDiff } from "@/hooks/useConfigDiff";
import { StepChoose } from "./StepChoose";
import { StepEdit } from "./StepEdit";
import { StepCompare } from "./StepCompare";
const STEPS = [
{ label: "Choose" },
{ label: "Clone & Edit" },
{ label: "Compare" },
];
export function ConfigDiff() {
const {
mode, clone, original, refs, diffs, leaked,
handleClone, handleReset, handleModeSwitch, handleFieldChange,
} = useConfigDiff();
const [step, setStep] = useState(0);
const goNext = () => {
if (step === 0) { handleClone(); setStep(1); }
else if (step < 2) { setStep(step + 1); }
};
const goBack = () => { if (step > 0) setStep(step - 1); };
const restart = () => { handleReset(); setStep(0); };
return (
<div className="h-screen flex flex-col bg-[#0c0c0f] text-white overflow-hidden relative">
<div className="absolute inset-0 bg-gradient-to-br from-violet-950/20 via-transparent to-fuchsia-950/10 pointer-events-none" />
{/* Header */}
<div className="shrink-0 border-b border-white/[0.06] relative z-10">
<div className="max-w-3xl mx-auto px-3 sm:px-4 h-12 flex items-center justify-between">
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full bg-violet-500" />
<span className="text-sm font-bold tracking-tight">Runway</span>
<span className="hidden sm:inline text-[10px] text-white/30 ml-3">Prototype pattern</span>
</div>
<div className="flex items-center gap-2">
<span className={`text-[10px] font-mono px-2 py-0.5 rounded-md border ${
mode === "shallow"
? "text-red-400 bg-red-500/10 border-red-500/20"
: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20"
}`}>
{mode === "shallow" ? "{ ...spread }" : "deepClone()"}
</span>
<button onClick={restart} className="p-1.5 rounded-md text-white/30 hover:text-white/60 hover:bg-white/[0.04] transition-colors" title="Start over">
<RotateCcw className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
{/* Stepper */}
<div className="shrink-0 border-b border-white/[0.04] relative z-10">
<div className="max-w-3xl mx-auto px-3 sm:px-4 py-3">
<div className="flex items-center justify-center">
{STEPS.map((s, i) => (
<div key={s.label} className="flex items-center">
<button
onClick={() => i <= step && setStep(i)}
className={`flex items-center gap-1.5 shrink-0 ${i <= step ? "cursor-pointer" : "cursor-default"}`}
>
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold transition-all ${
i < step ? "bg-violet-500 text-white"
: i === step ? "bg-violet-500/20 text-violet-400 border border-violet-500/50"
: "bg-white/[0.04] text-white/20 border border-white/[0.06]"
}`}>
{i < step ? <Check className="w-3 h-3" /> : i + 1}
</div>
<span className={`text-[11px] font-medium ml-1.5 hidden sm:inline ${i <= step ? "text-white/70" : "text-white/20"}`}>
{s.label}
</span>
</button>
{i < STEPS.length - 1 && (
<div className={`w-12 sm:w-20 h-px mx-2 sm:mx-3 ${i < step ? "bg-violet-500/50" : "bg-white/[0.06]"}`} />
)}
</div>
))}
</div>
</div>
</div>
{/* Content */}
<div className="flex-1 min-h-0 overflow-auto relative z-10">
<div className="max-w-3xl mx-auto px-3 sm:px-4 py-4 h-full flex flex-col">
{step === 0 && <StepChoose mode={mode} original={original} onModeSwitch={handleModeSwitch} />}
{step === 1 && <StepEdit original={original} clone={clone} mode={mode} leaked={leaked} onFieldChange={handleFieldChange} />}
{step === 2 && <StepCompare refs={refs} diffs={diffs} leaked={leaked} />}
</div>
</div>
{/* Bottom nav */}
<div className="shrink-0 border-t border-white/[0.06] relative z-10">
<div className="max-w-3xl mx-auto px-3 sm:px-4 py-3 flex items-center justify-between">
<button onClick={goBack} disabled={step === 0} className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium text-white/40 hover:text-white/60 disabled:opacity-20 disabled:cursor-default transition-colors">
<ChevronLeft className="w-3.5 h-3.5" /> Back
</button>
<div className="text-[10px] text-white/15">
<code className="text-white/20">deepClone()</code> from Arkhe
</div>
{step < 2 ? (
<button onClick={goNext} className="flex items-center gap-1.5 px-4 py-1.5 bg-violet-600 text-white rounded-md text-xs font-medium hover:bg-violet-500 shadow-lg shadow-violet-600/20 transition-all">
{step === 0 ? "Clone" : "Compare"} <ChevronRight className="w-3.5 h-3.5" />
</button>
) : (
<button onClick={restart} className="flex items-center gap-1.5 px-4 py-1.5 bg-white/[0.06] text-white/60 rounded-md text-xs font-medium hover:bg-white/[0.08] transition-colors">
<RotateCcw className="w-3.5 h-3.5" /> Try again
</button>
)}
</div>
</div>
</div>
);
}
Real-World Analogyβ
A document template. You don't write every letter from scratch β you copy a template and fill in the specifics. The template is the prototype.
When to Use Itβ
Whenever you need variations of a complex nested object β config overrides, test fixtures, game entity spawning, form presets. If the object has nested references, deepClone guarantees isolation. Shallow spread doesn't.
When NOT to Use Itβ
If your object is flat (no nesting), the spread operator { ...obj } is simpler and faster. Don't deep-clone when a shallow copy is sufficient.
APIβ
These functions are from Arkhe and re-exported by Eidos:
- deepClone β Deep clone with common types (objects, arrays, dates, maps, sets)
- deepCloneFull β Deep clone including binary data (TypedArrays, ArrayBuffer, Blob, File)