immer icon indicating copy to clipboard operation
immer copied to clipboard

Unexpected undefined not assigned

Open Lin-coln opened this issue 4 months ago • 2 comments

🐛 Bug Report

When I assign "undefined" to the prop "foo" of draft, and the prototype of base has the prop "foo" which is "undefined", the final return value is not assigned an "undefined" to the prop "foo".

To Reproduce

import { enablePatches, immerable, produceWithPatches } from "immer";

const proto = { [immerable]: true, name: undefined };
const foo = Object.create(proto);

console.log(`[hasOwnProp] foo:`, Object.prototype.hasOwnProperty.call(foo, "name"));

enablePatches();
const [foo_next, patches, _] = produceWithPatches(foo, (x) => {
  console.log(`[immer] produce foo_next from immer`);
  x.name = undefined;
});

console.log(`[immer] foo_next patches:`, patches);

console.log(`[hasOwnProp] foo:`, Object.prototype.hasOwnProperty.call(foo, "name"));
console.log(`[hasOwnProp] foo_next:`, Object.prototype.hasOwnProperty.call(foo_next, "name"));

foo.name = undefined;
console.log(`[vanilla] assign name manually`);
console.log(`[hasOwnProp] foo:`, Object.prototype.hasOwnProperty.call(foo, "name"));

Observed behavior

[hasOwnProp] foo: false
[immer] produce foo_next from immer
[immer] foo_next patches: []
[hasOwnProp] foo: false
[hasOwnProp] foo_next: false
[vanilla] assign name manually
[hasOwnProp] foo: true

Expected behavior

[hasOwnProp] foo: false
[immer] produce foo_next from immer
[immer] foo_next patches: [
  {
    op: "add",
    path: [ "name" ],
    value: undefined,
  }
]
[hasOwnProp] foo: false
[hasOwnProp] foo_next: true
[vanilla] assign name manually
[hasOwnProp] foo: true

Lin-coln avatar Aug 15 '25 19:08 Lin-coln

Adjusting the code here may solve it

src/core/proxy.ts

Image

replace prop in state.copy_ to has(state.copy_, prop). In this way, the corresponding prop on the prototype will not be detected

Lin-coln avatar Aug 15 '25 19:08 Lin-coln

Note that in JSON undefined doesn't exist, and generally "undefined" fields are signaled by the absence of that field, so in spirit of the JSON spec I think the current behavior seems about right. Is there anything specific you need this for. Did you consider using null instead?

mweststrate avatar Nov 28 '25 09:11 mweststrate