Structured Clone Fallback For Older Runtimes
Use native structuredClone when available and fall back to a cycle-safe deep clone covering dates, regexes, maps, sets and typed arrays.
Code
export class CloneError extends Error {
constructor(message: string) {
super(message);
this.name = "DataCloneError";
}
}
function nativeClone<T>(value: T): T | undefined {
const candidate = (globalThis as { structuredClone?: <U>(input: U) => U }).structuredClone;
return typeof candidate === "function" ? candidate(value) : undefined;
}
export function deepClone<T>(value: T): T {
const cloned = nativeClone(value);
return cloned === undefined ? (cloneFallback(value, new WeakMap()) as T) : cloned;
}
function cloneFallback(value: unknown, seen: WeakMap<object, unknown>): unknown {
if (value === null) return null;
const type = typeof value;
if (type === "function" || type === "symbol") {
throw new CloneError("value of type " + type + " cannot be cloned");
}
if (type !== "object") return value;
const object = value as object;
const existing = seen.get(object);
if (existing !== undefined) return existing;
if (value instanceof Date) return new Date(value.getTime());
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
if (value instanceof Error) {
const copy = new Error(value.message);
copy.name = value.name;
copy.stack = value.stack;
seen.set(object, copy);
return copy;
}
if (value instanceof ArrayBuffer) return value.slice(0);
if (value instanceof DataView) {
const buffer = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
return new DataView(buffer);
}
if (ArrayBuffer.isView(value)) {
const view = value as unknown as {
buffer: ArrayBuffer;
byteOffset: number;
byteLength: number;
constructor: new (buffer: ArrayBuffer) => object;
};
const buffer = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
return new view.constructor(buffer);
}
if (value instanceof Map) {
const copy = new Map<unknown, unknown>();
seen.set(object, copy);
for (const [key, entry] of value) {
copy.set(cloneFallback(key, seen), cloneFallback(entry, seen));
}
return copy;
}
if (value instanceof Set) {
const copy = new Set<unknown>();
seen.set(object, copy);
for (const entry of value) copy.add(cloneFallback(entry, seen));
return copy;
}
if (Array.isArray(value)) {
const copy: unknown[] = [];
seen.set(object, copy);
for (const item of value) copy.push(cloneFallback(item, seen));
return copy;
}
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw new CloneError("unsupported object type");
}
const copy: Record<PropertyKey, unknown> = Object.create(prototype);
seen.set(object, copy);
for (const key of Reflect.ownKeys(object)) {
const descriptor = Object.getOwnPropertyDescriptor(object, key);
if (!descriptor || !descriptor.enumerable) continue;
if (!("value" in descriptor)) {
throw new CloneError("accessor properties cannot be cloned");
}
copy[key] = cloneFallback(descriptor.value, seen);
}
return copy;
}Report a problem with this exampleOpens a pre-filled issue on GitHub.