Closed MaddyGuthridge closed 1 month ago
Thanks for the report @MaddyGuthridge! I've tested this locally and I get the same issue.
After testing this a bit I think the easiest solution for this is to call structredClone
on the default value in the defaulted
coercion if its an object. For primatives this shouldn't happen because of the way that JS handles assignments.
export function defaulted<T, S>(
struct: Struct<T, S>,
fallback: any,
options: {
strict?: boolean
} = {}
): Struct<T, S> {
return coerce(struct, unknown(), (x) => {
// To avoid a pass-by-reference bug, we'll clone objects when encountered
// here, but the for performance avoid cloning primatives and functions
const f =
typeof fallback === 'function'
? fallback()
: typeof fallback === 'object'
? structuredClone(fallback)
: fallback
WDYT? cc: @arturmuller
@MaddyGuthridge thank you for the report!
@yeoffrey hmm. structuredClone
destroys an object's prototype chain, meaning that if someone uses some custom class as the default value, they will unexpectedly get a plain object on the other end.
Using a custom class is probably a very rare use case, but also doesn't seem impossible.
I personally exclusively use POJOs with Superstruct, but... I guess someone somewhere could be doing something that this change could completely break.
I am honestly just leaning towards a warning in the docs, pointing people towards using the factory function signature for non primitive objects. My guess is that it was added exactly for this reason, just not properly documented.
@arturmuller Alternatively we could accept only a callback function. That would be a breaking change but to me this feels like a bit of a foot gun 😅
Up to you!
I may actually have a suggestion for this: a library maintained by one of my coworkers jewire
has some code that it uses to clone JS objects whilst preserving prototype chains (link to the source code). Perhaps you could use something similar?
I may actually have a suggestion for this: a library maintained by one of my coworkers
jewire
has some code that it uses to clone JS objects whilst preserving prototype chains (link to the source code). Perhaps you could use something similar?
This could be a good solution. I could try modifying #1248 to include that code. Thanks for the link!
This has been fixed by changes introduced by #1151 which was just released in v2.0.2. Thank you for the bug report!
When providing a default value for a
record
-type struct, the default value is not properly copied, meaning that it is at risk of subtle pass-by-reference bugs.Consider this example:
The current behaviour of a
defaulted(record(...), ...)
is different compared to that ofdefaulted(object(...), ...)
anddefaulted(array(...), ...)
which both appear to copy their input data.