typescript
56 lines · 8 steps
Building a deepClone with cycle safety
A generic deep clone that prefers the native structuredClone and falls back to a recursive walk that tracks already-seen objects.
Explained by
highlit
1type CloneInput = Record<string, unknown> | unknown[] | Map<unknown, unknown> | Set<unknown>;
2
3export function deepClone<T>(value: T): T {
4 if (typeof structuredClone === "function") {
5 return structuredClone(value);
6 }
7 return cloneFallback(value, new WeakMap());
8}
9
10function cloneFallback<T>(value: T, seen: WeakMap<object, unknown>): T {
11 if (value === null || typeof value !== "object") {
12 return value;
13 }
14
15 if (seen.has(value as object)) {
16 return seen.get(value as object) as T;
17 }
18
19 if (value instanceof Date) {
20 return new Date(value.getTime()) as T;
21 }
22
23 if (value instanceof RegExp) {
24 return new RegExp(value.source, value.flags) as T;
25 }
26
27 if (value instanceof Map) {
28 const result = new Map();
29 seen.set(value, result);
30 for (const [k, v] of value) {
31 result.set(cloneFallback(k, seen), cloneFallback(v, seen));
32 }
33 return result as T;
34 }
35
36 if (value instanceof Set) {
37 const result = new Set();
38 seen.set(value, result);
39 for (const v of value) result.add(cloneFallback(v, seen));
40 return result as T;
41 }
42
43 if (Array.isArray(value)) {
44 const result: unknown[] = [];
45 seen.set(value, result);
46 value.forEach((item, i) => (result[i] = cloneFallback(item, seen)));
47 return result as T;
48 }
49
50 const result: Record<string, unknown> = {};
51 seen.set(value as object, result);
52 for (const key of Reflect.ownKeys(value as object)) {
53 result[key as string] = cloneFallback((value as Record<string, unknown>)[key as string], seen);
54 }
55 return result as T;
56}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Reaching for a native primitive like structuredClone first keeps the fast path simple and correct.
- 2A WeakMap of seen objects lets recursion handle cyclic and shared references without infinite loops.
- 3Each container type needs its own clone strategy because copying their structure differs fundamentally.
Related explainers
typescript
import { CallHandler, ExecutionContext, Injectable,
Wrapping responses in a NestJS interceptor
interceptors
rxjs
response-shaping
Intermediate
7 steps
typescript
type RetryOptions = { retries?: number; timeoutMs?: number; baseDelayMs?: number;
Retry with timeout and backoff in TypeScript
promises
retry
exponential-backoff
Intermediate
10 steps
python
from collections.abc import Mapping from typing import Any, Iterator
Flattening nested config into dotted keys
recursion
generators
tree-traversal
Intermediate
7 steps
typescript
import { Pipe, PipeTransform, ChangeDetectorRef, NgZone, OnDestroy } from '@angular/core'; @Pipe({ name: 'timeAgo',
A self-refreshing timeAgo pipe in Angular
impure-pipe
change-detection
timers
Advanced
10 steps
typescript
const DIVISIONS: { amount: number; unit: Intl.RelativeTimeFormatUnit }[] = [ { amount: 60, unit: "seconds" }, { amount: 60, unit: "minutes" }, { amount: 24, unit: "hours" },
Human-readable relative times with Intl
internationalization
date-formatting
lookup-table
Intermediate
7 steps
ruby
require 'json' require 'set' class SensitiveScrubber
Recursively scrubbing secrets from JSON
recursion
data-masking
pattern-matching
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-deepclone-with-cycle-safety-explained-typescript-13ad/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.