#6215·lodash

cloneDeep strips Temporal.* instances to {} (loses prototype, internal slots, instanceof)

Author: glounderCreated May 20, 2026Updated Jul 12, 2026
Labelsenhancement

cloneDeep does not preserve Temporal.* instances. The clone loses its prototype chain, internal slots, and instanceof identity — the result is a plain {} with no Temporal methods. isEqual(original, clone) then returns false, which breaks downstream patterns that depend on "clone then compare" (dirty-state checks, undo/redo snapshots, form models, etc.).

Reproduction

import { cloneDeep, isEqual } from 'lodash';
import 'temporal-polyfill/global';

const date = Temporal.PlainDate.from('2026-05-20');
const clone = cloneDeep(date);

console.log(clone);                              // {}
console.log(clone instanceof Temporal.PlainDate); // false
console.log(isEqual(date, clone));                // false

Affects every Temporal value type: PlainDate, PlainTime, PlainDateTime, PlainYearMonth, PlainMonthDay, Instant, ZonedDateTime, Duration.

Environment

  • lodash 4.18.1
  • temporal-polyfill 0.3.2 (same shape will hit native Temporal once browsers ship it — the data lives in internal slots, not enumerable own properties)
  • Node 24 (also reproduces in jsdom / happy-dom / browser)

Why this matters

Temporal types are immutable, so the correct treatment is to return the same reference (no copy needed). lodash currently treats them as plain objects and walks own properties — which on a Temporal instance is the empty set, so the prototype, internal slots, and instanceof identity are all lost.

Workaround

cloneDeepWith with a customizer that short-circuits Temporal instances:

import { cloneDeepWith } from 'lodash';

const isTemporal = (v) =>
  v != null && typeof v === 'object' && (
    v instanceof Temporal.PlainDate ||
    v instanceof Temporal.PlainTime ||
    v instanceof Temporal.PlainDateTime ||
    v instanceof Temporal.PlainYearMonth ||
    v instanceof Temporal.PlainMonthDay ||
    v instanceof Temporal.Instant ||
    v instanceof Temporal.ZonedDateTime ||
    v instanceof Temporal.Duration
  );

export const cloneDeep = (value) =>
  cloneDeepWith(value, (v) => (isTemporal(v) ? v : undefined));

This works but means every consumer of Temporal-bearing state has to swap their import. A built-in fix would let cloneDeep Just Work for the upcoming Stage 3 Temporal proposal.

Suggested fix

Detect Temporal instances in the clone walker and return them by reference (they're immutable). A symmetric fix in isEqual would also help — isEqual(PlainDate.from('2026-05-20'), PlainDate.from('2026-05-20')) currently returns false because the Temporal Object.prototype.toString tag isn't in equalByTag's known list.