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 { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ValidationPipe } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger';
Wiring validation and Swagger docs in NestJS
validation
openapi
decorators
Intermediate
8 steps
typescript
import { Component, Input } from '@angular/core'; interface Order { id: string;
How Angular ICU plurals localize an order summary
i18n
pluralization
standalone-component
Intermediate
8 steps
java
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;
Building a trie for autocomplete in Java
trie
prefix-tree
recursion
Intermediate
8 steps
ruby
class KeyTransformer def self.camelize(data) new.camelize(data) end
Recursively camelizing nested Ruby data
recursion
data-transformation
pattern-matching
Intermediate
7 steps
typescript
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable, catchError, concatMap, finalize } from 'rxjs'; import { DataSource, QueryRunner } from 'typeorm';
Wrapping requests in a transaction with NestJS
interceptors
transactions
rxjs
Advanced
7 steps
typescript
type CsvColumn<T> = { header: string; value: (row: T) => string | number | boolean | null | undefined; };
Building a type-safe CSV writer in TypeScript
generics
serialization
escaping
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.