diff --git a/src/structs/coercions.ts b/src/structs/coercions.ts index 62566c50..ee2f8d10 100644 --- a/src/structs/coercions.ts +++ b/src/structs/coercions.ts @@ -43,7 +43,14 @@ export function defaulted( } = {} ): Struct { return coerce(struct, unknown(), (x) => { - const f = typeof fallback === 'function' ? fallback() : fallback + // 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 if (x === undefined) { return f diff --git a/test/defaultedRecord.test.ts b/test/defaultedRecord.test.ts new file mode 100644 index 00000000..1acf5f98 --- /dev/null +++ b/test/defaultedRecord.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from 'vitest' +import { create, defaulted, record, string } from '../src' + +test('Defaulted record value is an empty object', () => { + const DefaultedRecord = defaulted(record(string(), string()), {}) + + const recordA = create(undefined, DefaultedRecord) + const recordB = create(undefined, DefaultedRecord) + + // Shouldn't change recordB + recordA.name = 'maddy' + + expect(recordB.name).toBeUndefined() +}) + +test('Defaulted record value is an empty object returned by a function', () => { + const DefaultedRecordFn = defaulted(record(string(), string()), () => ({})) + + const recordA = create(undefined, DefaultedRecordFn) + const recordB = create(undefined, DefaultedRecordFn) + + // Shouldn't change recordB + recordA.name = 'george' + + expect(recordB.name).toBeUndefined() +})