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.

TypescriptData Integrity

Code

snippet
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;
}