Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,15 @@ const handleRecord = (
`ar${i}v[ar${i}s[i]]=${mirror(child, `ar${i}p`, instruction)}`

const optionals = instruction.optionalsInArray[i + 1]
if (optionals)
if (optionals) {
for (let oi = 0; oi < optionals.length; oi++) {
const target = `ar${i}v[ar${i}s[i]]${optionals[oi]}`

v += `;if(${target}===undefined)delete ${target}`
}
// Clear the optionals array after use to prevent pollution across sibling arrays
instruction.optionalsInArray[i + 1] = []
}

v += `}` + `return ar${i}v` + `})()`

Expand Down Expand Up @@ -394,8 +397,11 @@ const mirror = (
}
const array = instruction.optionalsInArray

if (array[index]) array[index].push(refName)
else array[index] = [refName]
if (array[index]) {
array[index].push(refName)
} else {
array[index] = [refName]
}
} else {
instruction.optionals.push(name)
}
Expand Down Expand Up @@ -481,6 +487,8 @@ const mirror = (
// we can add semi-colon here because it delimit recursive mirror
v += `;if(${target}===undefined)delete ${target}`
}
// Clear the optionals array after use to prevent pollution across sibling arrays
instruction.optionalsInArray[i + 1] = []
}

v += `}`
Expand Down
105 changes: 105 additions & 0 deletions test/array-nested-optionals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { expect, test, describe } from 'bun:test'
import { Type } from '@sinclair/typebox'
import { TypeCompiler } from '@sinclair/typebox/compiler'
import { createMirror } from '../src/index'

describe('Nested Array with Optional Properties', () => {
test('should preserve array items when cleaning nested arrays', () => {
const WeightSchema = Type.Object({
amount: Type.Number(),
unit: Type.Union([
Type.Literal('g'),
Type.Literal('oz'),
Type.Literal('lb'),
Type.Literal('kg')
])
})

const PourSchema = Type.Object({
weight: WeightSchema,
time: Type.Number()
})

const ResponseSchema = Type.Object({
data: Type.Array(
Type.Object({
id: Type.String(),
pours: Type.Union([Type.Null(), Type.Array(PourSchema)]),
tags: Type.Array(Type.Object({ name: Type.String() })),
createdAt: Type.Transform(
Type.Union([Type.Date(), Type.String()])
)
.Decode((v) => (v instanceof Date ? v : new Date(v)))
.Encode((v) => v.toISOString())
})
)
})

const clean = createMirror(ResponseSchema, { TypeCompiler })

const input = {
data: [
{
id: 'test-1',
pours: null,
tags: [{ name: 'test' }],
createdAt: new Date('2025-01-01'),
extraProp: 'should-be-removed'
}
]
}

const result = clean(input)

// Array items should be preserved
expect(result.data).toHaveLength(1)
expect(result.data[0].id).toBe('test-1')
expect(result.data[0].pours).toBe(null)
expect(result.data[0].tags).toHaveLength(1)
expect(result.data[0].tags[0].name).toBe('test')

// Extra property should be removed
expect(result.data[0]).not.toHaveProperty('extraProp')
})

test('should handle multiple nested arrays correctly', () => {
const Schema = Type.Object({
outer: Type.Array(
Type.Object({
middle: Type.Array(
Type.Object({
inner: Type.Array(Type.String()),
value: Type.String()
})
)
})
)
})

const clean = createMirror(Schema, { TypeCompiler })

const input = {
outer: [
{
middle: [
{
inner: ['a', 'b'],
value: 'test',
extra: 'remove'
}
],
extraOuter: 'remove'
}
]
}

const result = clean(input)

expect(result.outer).toHaveLength(1)
expect(result.outer[0].middle).toHaveLength(1)
expect(result.outer[0].middle[0].inner).toHaveLength(2)
expect(result.outer[0].middle[0].value).toBe('test')
expect(result.outer[0].middle[0]).not.toHaveProperty('extra')
expect(result.outer[0]).not.toHaveProperty('extraOuter')
})
})