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 { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 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.