Skip to content

Commit 0fa2d7d

Browse files
committed
fix: prevent subtype reduction from collapsing collection and paginator return types
When a controller returns either a collection or paginator from the serialize method, TypeScript's subtype reduction collapses the union because the paginator type (with metadata) is structurally assignable to the collection type (without metadata). Add `metadata?: never` to the wrapped collection serialize overload to make the two types structurally incompatible, preserving the union.
1 parent c3e5f81 commit 0fa2d7d

File tree

2 files changed

+54
-5
lines changed

2 files changed

+54
-5
lines changed

src/base_serializer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,11 @@ export abstract class BaseSerializer<
189189
serialize<ResourceCollection extends CollectionContract<any, any, any>>(
190190
collection: ResourceCollection,
191191
resolver?: ContainerResolver<any>
192-
): Promise<UnpackAsTopLevelCollection<ResourceCollection, Wrappers['Wrap']>>
192+
): Promise<
193+
Wrappers['Wrap'] extends string
194+
? UnpackAsTopLevelCollection<ResourceCollection, Wrappers['Wrap']> & { metadata?: never }
195+
: UnpackAsTopLevelCollection<ResourceCollection, Wrappers['Wrap']>
196+
>
193197

194198
/**
195199
* Serializes a Paginator resource into paginated data with metadata.

tests/transformer.spec.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,10 +1574,10 @@ test.group('Transformer | wrapping', () => {
15741574
)
15751575
type UserData = InferData<UserTransformer>
15761576

1577-
expectTypeOf(userData).toEqualTypeOf<{ data: UserData[] }>()
1578-
expectTypeOf(userData).toEqualTypeOf<{
1579-
data: { id: number; fullName: string | null; email: string }[]
1580-
}>()
1577+
expectTypeOf(userData).toEqualTypeOf<{ data: UserData[] } & { metadata?: never }>()
1578+
expectTypeOf(userData).toEqualTypeOf<
1579+
{ data: { id: number; fullName: string | null; email: string }[] } & { metadata?: never }
1580+
>()
15811581

15821582
assert.deepEqual(userData, { data: [{ id: 1, fullName: null, email: 'foo@bar.com' }] })
15831583
})
@@ -1627,6 +1627,51 @@ test.group('Transformer | wrapping', () => {
16271627
})
16281628
})
16291629

1630+
test('return type is a union when controller returns collection or paginator', async ({
1631+
expectTypeOf,
1632+
}) => {
1633+
class User {
1634+
declare id: number
1635+
declare fullName: string | null
1636+
declare email: string
1637+
}
1638+
class UserTransformer extends BaseTransformer<User> {
1639+
toObject() {
1640+
return {
1641+
id: this.resource.id,
1642+
fullName: this.resource.fullName,
1643+
email: this.resource.email,
1644+
}
1645+
}
1646+
}
1647+
1648+
type UserData = InferData<UserTransformer>
1649+
1650+
async function handler(cond: boolean) {
1651+
if (cond) {
1652+
return wrappedApiSerializer.serialize(
1653+
UserTransformer.paginate([new User()], {}),
1654+
container.createResolver()
1655+
)
1656+
}
1657+
return wrappedApiSerializer.serialize(
1658+
UserTransformer.transform([new User()]),
1659+
container.createResolver()
1660+
)
1661+
}
1662+
1663+
type Result = Awaited<ReturnType<typeof handler>>
1664+
1665+
expectTypeOf<Result>().not.toEqualTypeOf<{ data: UserData[] } & { metadata?: never }>()
1666+
expectTypeOf<Result>().toEqualTypeOf<
1667+
| ({ data: UserData[] } & { metadata?: never })
1668+
| {
1669+
data: UserData[]
1670+
metadata: { currentPage: number; totalItems: number }
1671+
}
1672+
>()
1673+
})
1674+
16301675
test('only wrap top-level resources', async ({ assert, expectTypeOf }) => {
16311676
class User {
16321677
declare id: number

0 commit comments

Comments
 (0)