sindresorhus / is

Type check values
MIT License
1.68k stars 109 forks source link

`plainObject` is incompatible with `structuredClone` #183

Closed JamieMagee closed 1 year ago

JamieMagee commented 1 year ago

structuredClone was added as a global function in Node 17 as a built-in way to deep-clone objects. However, one of the limitations of the structured clone algorithm is that it doesn't clone the prototype chain^1. isPlainObject explicitly compares values with the prototype of Object^2 which means that objects cloned with structuredClone always return false. This is called out in the documentation, so I don't necessarily think it's a bug

An object is plain if it's created by either {}, new Object(), or Object.create(null).

But I also think that is.object is too broad, and is.emptyObject/is.nonEmptyObject are perhaps too precise. Some options I've considered:

  1. is.plainObject should return true for objects cloned with structuredClone?
  2. There should be a new function is.clonedObject?
  3. Some other possibility I'm missing?

But I would like to get your input.

Here's a small repro:

import is from '@sindresorhus/is';

const myObj = {
    a: 1,
    b: 2,
}

console.log(`is.plainObject: ${is.plainObject(myObj)}`) // is.plainObject: true
console.log(`is.object: ${is.object(myObj)}`) // is.object: true
console.log(`is.nonEmptyObject: ${is.nonEmptyObject(myObj)}`) // is.nonEmptyObject: true

const myStructuredClonedObject = structuredClone(myObj);

console.log(`is.plainObject: ${is.plainObject(myStructuredClonedObject)}`) // is.plainObject: false
console.log(`is.object: ${is.object(myStructuredClonedObject)}`) // is.object: true
console.log(`is.nonEmptyObject: ${is.nonEmptyObject(myStructuredClonedObject)}`) // is.nonEmptyObject: true

const myJsonClonedObject = JSON.parse(JSON.stringify(myObj));

console.log(`is.plainObject: ${is.plainObject(myJsonClonedObject)}`) // is.plainObject: true
console.log(`is.object: ${is.object(myJsonClonedObject)}`) // is.object: true
console.log(`is.nonEmptyObject: ${is.nonEmptyObject(myJsonClonedObject)}`) // is.nonEmptyObject: true
sindresorhus commented 1 year ago

The provided code seems to pass for me. I have added a test for this that passes: https://github.com/sindresorhus/is/commit/9265e9072d512ab1188c1d6347bc46c287775a93

JamieMagee commented 1 year ago

I think the root cause of this may be me using version 4.x, and plainObject improvements here: https://github.com/sindresorhus/is/commit/45cae31f4d7311d8ae1250558e710594acff7d01

Thanks for the help