Skip to content

Pass non-plain object values (Date, Map, ...) through in change-case/keys - #368

Open
yfwmaniish wants to merge 1 commit into
blakeembrey:mainfrom
yfwmaniish:fix-keys-pass-through-non-plain-objects
Open

yfwmaniish wants to merge 1 commit into
blakeembrey:mainfrom
yfwmaniish:fix-keys-pass-through-non-plain-objects

Conversation

@yfwmaniish

Copy link
Copy Markdown

Closes #356.

Problem

change-case/keys recurses into every value that is typeof === "object", including built-ins like Date, Map, Set, and RegExp. For those it builds a shallow clone with Object.create(Object.getPrototypeOf(object)) and copies own enumerable keys — but built-ins carry their state in internal slots, not enumerable properties, so the "clone" is a broken husk with the right prototype and none of the internals.

For a Date this is easy to hit (from #356):

import { camelCase } from "change-case/keys";

const out = camelCase({ created_at: new Date() }, 2);
out.createdAt.toISOString();
// ❌ TypeError: Method Date.prototype.toISOString called on incompatible receiver [object Object]

out.createdAt is Date {} — a prototype-only object with no [[DateValue]]. The same breakage applies to Map, Set, RegExp, and any class instance that relies on internal slots.

Fix

Only recurse into arrays and plain objects; pass any other object through unchanged as a value. Plain-object detection is a prototype check (Object.prototype or null), so Object.create(null) bags still get transformed, and nested plain objects/arrays are unaffected.

const isPlainObject = (object: unknown): boolean => {
  if (object === null || typeof object !== "object") return false;
  const prototype = Object.getPrototypeOf(object);
  return prototype === null || prototype === Object.prototype;
};
-    if (depth === 0 || !isObject(object)) return object;
+    if (depth === 0) return object;

     if (Array.isArray(object)) {
       return object.map((item) => changeKeys(item, depth - 1, options));
     }

+    if (!isPlainObject(object)) return object;

Behavior

Input value Before After
plain object keys transformed keys transformed (unchanged)
array mapped mapped (unchanged)
Object.create(null) keys transformed keys transformed (unchanged)
Date / Map / Set / RegExp broken empty clone passed through as-is

Tests

Added a keys.spec.ts case asserting Date, Map, and RegExp values are passed through by reference and remain functional (toISOString() works). Full change-case suite passes (28 tests), typecheck and Prettier clean.

Copilot AI lite review requested due to automatic review settings September 5, 2026 17:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

change-case/keys fails on Date objects

2 participants