Skip to content

Commit ecbf2be

Browse files
committed
fix: replace MultipartFile with File | Blob in client body types
1 parent 8a547b1 commit ecbf2be

3 files changed

Lines changed: 52 additions & 1 deletion

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tuyau/core': patch
3+
---
4+
5+
Replace `MultipartFile` with `File | Blob` in client-side body types.
6+
7+
When using `vine.file()` in validators, the generated types previously exposed `MultipartFile` (a server-side type) to the client, forcing users to cast it to `File` or `Blob` when handling file uploads on the client. With this change,`ExtractBody` now automatically replaces `MultipartFile` with `File | Blob`.

packages/core/src/client/types/types.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,27 @@ export type ExtractQuery<T> = 'query' extends keyof T
7575
*/
7676
export type ExtractQueryForGet<T> = DistributiveOmit<T, 'headers' | 'cookies' | 'params'>
7777

78+
/**
79+
* Recursively replaces AdonisJS MultipartFile types with the DOM File type.
80+
* MultipartFile is matched structurally via `{ isMultipartFile: true }` to avoid
81+
* importing from @adonisjs/bodyparser in client-side code.
82+
*/
83+
type ReplaceMultipartFile<T> = T extends { isMultipartFile: true }
84+
? File | Blob
85+
: T extends (infer U)[]
86+
? ReplaceMultipartFile<U>[]
87+
: T extends object
88+
? { [K in keyof T]: ReplaceMultipartFile<T[K]> }
89+
: T
90+
7891
/**
7992
* Extract body from a validator type, excluding reserved properties.
8093
* Excludes 'query', 'params', 'headers', and 'cookies' as these are handled separately by AdonisJS.
94+
* Also replaces MultipartFile types with DOM File for client-side usage.
8195
*/
82-
export type ExtractBody<T> = DistributiveOmit<T, 'query' | 'params' | 'headers' | 'cookies'>
96+
export type ExtractBody<T> = ReplaceMultipartFile<
97+
DistributiveOmit<T, 'query' | 'params' | 'headers' | 'cookies'>
98+
>
8399

84100
/**
85101
* Success status codes (2xx)

packages/core/tests/typings.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,34 @@ test.group('ExtractQuery and ExtractBody types', (group) => {
924924
password: string
925925
}>()
926926
})
927+
928+
test('ExtractBody replaces MultipartFile with File | Blob', ({ expectTypeOf }) => {
929+
type WithFile = { avatar: { isMultipartFile: true; fieldName: string; size: number }; name: string }
930+
931+
type Result = ExtractBody<WithFile>
932+
expectTypeOf<Result>().toEqualTypeOf<{ avatar: File | Blob; name: string }>()
933+
})
934+
935+
test('ExtractBody replaces MultipartFile array with (File | Blob) array', ({ expectTypeOf }) => {
936+
type WithFiles = { documents: { isMultipartFile: true; fieldName: string }[]; title: string }
937+
938+
type Result = ExtractBody<WithFiles>
939+
expectTypeOf<Result>().toEqualTypeOf<{ documents: (File | Blob)[]; title: string }>()
940+
})
941+
942+
test('ExtractBody replaces optional MultipartFile with optional File | Blob', ({ expectTypeOf }) => {
943+
type WithOptionalFile = { avatar?: { isMultipartFile: true; fieldName: string }; name: string }
944+
945+
type Result = ExtractBody<WithOptionalFile>
946+
expectTypeOf<Result>().toEqualTypeOf<{ avatar?: File | Blob; name: string }>()
947+
})
948+
949+
test('ExtractBody does not affect non-file types', ({ expectTypeOf }) => {
950+
type NoFiles = { name: string; age: number; active: boolean }
951+
952+
type Result = ExtractBody<NoFiles>
953+
expectTypeOf<Result>().toEqualTypeOf<{ name: string; age: number; active: boolean }>()
954+
})
927955
})
928956

929957
test.group('ExtractResponse type', (group) => {

0 commit comments

Comments
 (0)