diff --git a/deno_dist/request.ts b/deno_dist/request.ts
index 6fd80d42b..c74d1d2d3 100644
--- a/deno_dist/request.ts
+++ b/deno_dist/request.ts
@@ -183,11 +183,15 @@ export class HonoRequest
{
* ```
* @see https://hono.dev/api/request#parsebody
*/
- async parseBody(options?: ParseBodyOptions): Promise {
+ async parseBody, T extends BodyData>(
+ options?: Options
+ ): Promise
+ async parseBody(options?: Partial): Promise
+ async parseBody(options?: Partial) {
if (this.bodyCache.parsedBody) {
- return this.bodyCache.parsedBody as T
+ return this.bodyCache.parsedBody
}
- const parsedBody = await parseBody(this, options)
+ const parsedBody = await parseBody(this, options)
this.bodyCache.parsedBody = parsedBody
return parsedBody
}
diff --git a/deno_dist/utils/body.ts b/deno_dist/utils/body.ts
index db47b8c23..c4b7a7287 100644
--- a/deno_dist/utils/body.ts
+++ b/deno_dist/utils/body.ts
@@ -1,7 +1,39 @@
import { HonoRequest } from '../request.ts'
-type BodyDataValue = string | File | (string | File)[] | { [key: string]: BodyDataValue }
-export type BodyData = Record
+type BodyDataValueDot = { [x: string]: string | File | BodyDataValueDot } & {}
+type BodyDataValueDotAll = {
+ [x: string]: string | File | (string | File)[] | BodyDataValueDotAll
+} & {}
+type SimplifyBodyData = {
+ [K in keyof T]: string | File | (string | File)[] | BodyDataValueDotAll extends T[K]
+ ? string | File | (string | File)[] | BodyDataValueDotAll
+ : string | File | BodyDataValueDot extends T[K]
+ ? string | File | BodyDataValueDot
+ : string | File | (string | File)[] extends T[K]
+ ? string | File | (string | File)[]
+ : string | File
+} & {}
+
+type BodyDataValueComponent =
+ | string
+ | File
+ | (T extends { all: false }
+ ? never // explicitly set to false
+ : T extends { all: true } | { all: boolean }
+ ? (string | File)[] // use all option
+ : never) // without options
+type BodyDataValueObject = { [key: string]: BodyDataValueComponent | BodyDataValueObject }
+type BodyDataValue =
+ | BodyDataValueComponent
+ | (T extends { dot: false }
+ ? never // explicitly set to false
+ : T extends { dot: true } | { dot: boolean }
+ ? BodyDataValueObject // use dot option
+ : never) // without options
+export type BodyData = {}> = SimplifyBodyData<
+ Record>
+>
+
export type ParseBodyOptions = {
/**
* Determines whether all fields with multiple values should be parsed as arrays.
@@ -44,10 +76,20 @@ export type ParseBodyOptions = {
* @param {Partial} [options] - Options for parsing the body.
* @returns {Promise} The parsed body data.
*/
-export const parseBody = async (
+interface ParseBody {
+ , T extends BodyData>(
+ request: HonoRequest | Request,
+ options?: Options
+ ): Promise
+ (
+ request: HonoRequest | Request,
+ options?: Partial
+ ): Promise
+}
+export const parseBody: ParseBody = async (
request: HonoRequest | Request,
- options: Partial = Object.create(null)
-): Promise => {
+ options = Object.create(null)
+) => {
const { all = false, dot = false } = options
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers
@@ -57,10 +99,10 @@ export const parseBody = async (
(contentType !== null && contentType.startsWith('multipart/form-data')) ||
(contentType !== null && contentType.startsWith('application/x-www-form-urlencoded'))
) {
- return parseFormData(request, { all, dot })
+ return parseFormData(request, { all, dot })
}
- return {} as T
+ return {}
}
/**
@@ -71,7 +113,7 @@ export const parseBody = async (
* @param {ParseBodyOptions} options - Options for parsing the form data.
* @returns {Promise} The parsed body data.
*/
-async function parseFormData(
+async function parseFormData(
request: HonoRequest | Request,
options: ParseBodyOptions
): Promise {
@@ -92,7 +134,7 @@ async function parseFormData(
* @param {ParseBodyOptions} options - Options for parsing the form data.
* @returns {T} The converted body data.
*/
-function convertFormDataToBodyData(
+function convertFormDataToBodyData(
formData: FormData,
options: ParseBodyOptions
): T {
@@ -134,7 +176,11 @@ function convertFormDataToBodyData(
* @param {string} key - The key to parse.
* @param {FormDataEntryValue} value - The value to assign.
*/
-const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntryValue): void => {
+const handleParsingAllValues = (
+ form: BodyData<{ all: true }>,
+ key: string,
+ value: FormDataEntryValue
+): void => {
if (form[key] !== undefined) {
if (Array.isArray(form[key])) {
;(form[key] as (string | File)[]).push(value)
@@ -153,7 +199,11 @@ const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntr
* @param {string} key - The dot notation key.
* @param {BodyDataValue} value - The value to assign.
*/
-const handleParsingNestedValues = (form: BodyData, key: string, value: BodyDataValue): void => {
+const handleParsingNestedValues = (
+ form: BodyData,
+ key: string,
+ value: BodyDataValue>
+): void => {
let nestedForm = form
const keys = key.split('.')
@@ -169,7 +219,7 @@ const handleParsingNestedValues = (form: BodyData, key: string, value: BodyDataV
) {
nestedForm[key] = Object.create(null)
}
- nestedForm = nestedForm[key] as BodyData
+ nestedForm = nestedForm[key] as unknown as BodyData
}
})
}
diff --git a/deno_dist/validator/validator.ts b/deno_dist/validator/validator.ts
index 20571fd95..564375c2f 100644
--- a/deno_dist/validator/validator.ts
+++ b/deno_dist/validator/validator.ts
@@ -91,7 +91,7 @@ export const validator = <
try {
const arrayBuffer = await c.req.arrayBuffer()
const formData = await bufferToFormData(arrayBuffer, contentType)
- const form: BodyData = {}
+ const form: BodyData<{ all: true }> = {}
formData.forEach((value, key) => {
if (key.endsWith('[]')) {
if (form[key] === undefined) {
diff --git a/src/request.test.ts b/src/request.test.ts
index 9163f2a38..d73d38aae 100644
--- a/src/request.test.ts
+++ b/src/request.test.ts
@@ -1,6 +1,10 @@
import { HonoRequest } from './request'
import type { RouterRoute } from './types'
+type RecursiveRecord = {
+ [key in K]: T | RecursiveRecord
+}
+
describe('Query', () => {
test('req.query() and req.queries()', () => {
const rawRequest = new Request('http://localhost?page=2&tag=A&tag=B')
@@ -251,4 +255,67 @@ describe('Body methods with caching', () => {
expect(async () => await req.arrayBuffer()).not.toThrow()
expect(async () => await req.blob()).not.toThrow()
})
+
+ describe('req.parseBody()', async () => {
+ it('should parse form data', async () => {
+ const data = new FormData()
+ data.append('foo', 'bar')
+ const req = new HonoRequest(
+ new Request('http://localhost', {
+ method: 'POST',
+ body: data,
+ })
+ )
+ expect((await req.parseBody())['foo']).toBe('bar')
+ expect((await req.parseBody())['foo']).toBe('bar')
+ expect(async () => await req.text()).not.toThrow()
+ expect(async () => await req.arrayBuffer()).not.toThrow()
+ expect(async () => await req.blob()).not.toThrow()
+ })
+
+ describe('Return type', () => {
+ let req: HonoRequest
+ beforeEach(() => {
+ const data = new FormData()
+ data.append('foo', 'bar')
+ req = new HonoRequest(
+ new Request('http://localhost', {
+ method: 'POST',
+ body: data,
+ })
+ )
+ })
+
+ it('without options', async () => {
+ expectTypeOf((await req.parseBody())['key']).toEqualTypeOf()
+ })
+
+ it('{all: true}', async () => {
+ expectTypeOf((await req.parseBody({ all: true }))['key']).toEqualTypeOf<
+ string | File | (string | File)[]
+ >()
+ })
+
+ it('{dot: true}', async () => {
+ expectTypeOf((await req.parseBody({ dot: true }))['key']).toEqualTypeOf<
+ string | File | RecursiveRecord
+ >()
+ })
+
+ it('{all: true, dot: true}', async () => {
+ expectTypeOf((await req.parseBody({ all: true, dot: true }))['key']).toEqualTypeOf<
+ | string
+ | File
+ | (string | File)[]
+ | RecursiveRecord
+ >()
+ })
+
+ it('specify return type explicitly', async () => {
+ expectTypeOf(
+ await req.parseBody<{ key1: string; key2: string }>({ all: true, dot: true })
+ ).toEqualTypeOf<{ key1: string; key2: string }>()
+ })
+ })
+ })
})
diff --git a/src/request.ts b/src/request.ts
index 7c028aee5..291e7e233 100644
--- a/src/request.ts
+++ b/src/request.ts
@@ -183,11 +183,15 @@ export class HonoRequest {
* ```
* @see https://hono.dev/api/request#parsebody
*/
- async parseBody(options?: ParseBodyOptions): Promise {
+ async parseBody, T extends BodyData>(
+ options?: Options
+ ): Promise
+ async parseBody(options?: Partial): Promise
+ async parseBody(options?: Partial) {
if (this.bodyCache.parsedBody) {
- return this.bodyCache.parsedBody as T
+ return this.bodyCache.parsedBody
}
- const parsedBody = await parseBody(this, options)
+ const parsedBody = await parseBody(this, options)
this.bodyCache.parsedBody = parsedBody
return parsedBody
}
diff --git a/src/utils/body.test.ts b/src/utils/body.test.ts
index d04ed0801..6e049802d 100644
--- a/src/utils/body.test.ts
+++ b/src/utils/body.test.ts
@@ -1,4 +1,8 @@
-import { parseBody } from './body'
+import { parseBody, type BodyData } from './body'
+
+type RecursiveRecord = {
+ [key in K]: T | RecursiveRecord
+}
describe('Parse Body Util', () => {
const FORM_URL = 'https://localhost/form'
@@ -240,4 +244,110 @@ describe('Parse Body Util', () => {
expect(await parseBody(req)).toEqual({})
})
+
+ describe('Return type', () => {
+ let req: Request
+ beforeEach(() => {
+ req = createRequest(FORM_URL, 'POST', new FormData())
+ })
+
+ it('without options', async () => {
+ expectTypeOf((await parseBody(req))['key']).toEqualTypeOf()
+ })
+
+ it('{all: true}', async () => {
+ expectTypeOf((await parseBody(req, { all: true }))['key']).toEqualTypeOf<
+ string | File | (string | File)[]
+ >()
+ })
+
+ it('{all: boolean}', async () => {
+ expectTypeOf((await parseBody(req, { all: !!Math.random() }))['key']).toEqualTypeOf<
+ string | File | (string | File)[]
+ >()
+ })
+
+ it('{dot: true}', async () => {
+ expectTypeOf((await parseBody(req, { dot: true }))['key']).toEqualTypeOf<
+ string | File | RecursiveRecord
+ >()
+ })
+
+ it('{dot: boolean}', async () => {
+ expectTypeOf((await parseBody(req, { dot: !!Math.random() }))['key']).toEqualTypeOf<
+ string | File | RecursiveRecord
+ >()
+ })
+
+ it('{all: true, dot: true}', async () => {
+ expectTypeOf((await parseBody(req, { all: true, dot: true }))['key']).toEqualTypeOf<
+ | string
+ | File
+ | (string | File)[]
+ | RecursiveRecord
+ >()
+ })
+
+ it('{all: boolean, dot: boolean}', async () => {
+ expectTypeOf(
+ (await parseBody(req, { all: !!Math.random(), dot: !!Math.random() }))['key']
+ ).toEqualTypeOf<
+ | string
+ | File
+ | (string | File)[]
+ | RecursiveRecord
+ >()
+ })
+
+ it('specify return type explicitly', async () => {
+ expectTypeOf(
+ await parseBody<{ key1: string; key2: string }>(req, {
+ all: !!Math.random(),
+ dot: !!Math.random(),
+ })
+ ).toEqualTypeOf<{ key1: string; key2: string }>()
+ })
+ })
+})
+
+describe('BodyData', () => {
+ it('without options', async () => {
+ expectTypeOf(({} as BodyData)['key']).toEqualTypeOf()
+ })
+
+ it('{all: true}', async () => {
+ expectTypeOf(({} as BodyData<{ all: true }>)['key']).toEqualTypeOf<
+ string | File | (string | File)[]
+ >()
+ })
+
+ it('{all: boolean}', async () => {
+ expectTypeOf(({} as BodyData<{ all: boolean }>)['key']).toEqualTypeOf<
+ string | File | (string | File)[]
+ >()
+ })
+
+ it('{dot: true}', async () => {
+ expectTypeOf(({} as BodyData<{ dot: true }>)['key']).toEqualTypeOf<
+ string | File | RecursiveRecord
+ >()
+ })
+
+ it('{dot: boolean}', async () => {
+ expectTypeOf(({} as BodyData<{ dot: boolean }>)['key']).toEqualTypeOf<
+ string | File | RecursiveRecord
+ >()
+ })
+
+ it('{all: true, dot: true}', async () => {
+ expectTypeOf(({} as BodyData<{ all: true; dot: true }>)['key']).toEqualTypeOf<
+ string | File | (string | File)[] | RecursiveRecord
+ >()
+ })
+
+ it('{all: boolean, dot: boolean}', async () => {
+ expectTypeOf(({} as BodyData<{ all: boolean; dot: boolean }>)['key']).toEqualTypeOf<
+ string | File | (string | File)[] | RecursiveRecord
+ >()
+ })
})
diff --git a/src/utils/body.ts b/src/utils/body.ts
index 8013dd212..4f08ccdef 100644
--- a/src/utils/body.ts
+++ b/src/utils/body.ts
@@ -1,7 +1,39 @@
import { HonoRequest } from '../request'
-type BodyDataValue = string | File | (string | File)[] | { [key: string]: BodyDataValue }
-export type BodyData = Record
+type BodyDataValueDot = { [x: string]: string | File | BodyDataValueDot } & {}
+type BodyDataValueDotAll = {
+ [x: string]: string | File | (string | File)[] | BodyDataValueDotAll
+} & {}
+type SimplifyBodyData = {
+ [K in keyof T]: string | File | (string | File)[] | BodyDataValueDotAll extends T[K]
+ ? string | File | (string | File)[] | BodyDataValueDotAll
+ : string | File | BodyDataValueDot extends T[K]
+ ? string | File | BodyDataValueDot
+ : string | File | (string | File)[] extends T[K]
+ ? string | File | (string | File)[]
+ : string | File
+} & {}
+
+type BodyDataValueComponent =
+ | string
+ | File
+ | (T extends { all: false }
+ ? never // explicitly set to false
+ : T extends { all: true } | { all: boolean }
+ ? (string | File)[] // use all option
+ : never) // without options
+type BodyDataValueObject = { [key: string]: BodyDataValueComponent | BodyDataValueObject }
+type BodyDataValue =
+ | BodyDataValueComponent
+ | (T extends { dot: false }
+ ? never // explicitly set to false
+ : T extends { dot: true } | { dot: boolean }
+ ? BodyDataValueObject // use dot option
+ : never) // without options
+export type BodyData = {}> = SimplifyBodyData<
+ Record>
+>
+
export type ParseBodyOptions = {
/**
* Determines whether all fields with multiple values should be parsed as arrays.
@@ -44,10 +76,20 @@ export type ParseBodyOptions = {
* @param {Partial} [options] - Options for parsing the body.
* @returns {Promise} The parsed body data.
*/
-export const parseBody = async (
+interface ParseBody {
+ , T extends BodyData>(
+ request: HonoRequest | Request,
+ options?: Options
+ ): Promise
+ (
+ request: HonoRequest | Request,
+ options?: Partial
+ ): Promise
+}
+export const parseBody: ParseBody = async (
request: HonoRequest | Request,
- options: Partial = Object.create(null)
-): Promise => {
+ options = Object.create(null)
+) => {
const { all = false, dot = false } = options
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers
@@ -57,10 +99,10 @@ export const parseBody = async (
(contentType !== null && contentType.startsWith('multipart/form-data')) ||
(contentType !== null && contentType.startsWith('application/x-www-form-urlencoded'))
) {
- return parseFormData(request, { all, dot })
+ return parseFormData(request, { all, dot })
}
- return {} as T
+ return {}
}
/**
@@ -71,7 +113,7 @@ export const parseBody = async (
* @param {ParseBodyOptions} options - Options for parsing the form data.
* @returns {Promise} The parsed body data.
*/
-async function parseFormData(
+async function parseFormData(
request: HonoRequest | Request,
options: ParseBodyOptions
): Promise {
@@ -92,7 +134,7 @@ async function parseFormData(
* @param {ParseBodyOptions} options - Options for parsing the form data.
* @returns {T} The converted body data.
*/
-function convertFormDataToBodyData(
+function convertFormDataToBodyData(
formData: FormData,
options: ParseBodyOptions
): T {
@@ -134,7 +176,11 @@ function convertFormDataToBodyData(
* @param {string} key - The key to parse.
* @param {FormDataEntryValue} value - The value to assign.
*/
-const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntryValue): void => {
+const handleParsingAllValues = (
+ form: BodyData<{ all: true }>,
+ key: string,
+ value: FormDataEntryValue
+): void => {
if (form[key] !== undefined) {
if (Array.isArray(form[key])) {
;(form[key] as (string | File)[]).push(value)
@@ -153,7 +199,11 @@ const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntr
* @param {string} key - The dot notation key.
* @param {BodyDataValue} value - The value to assign.
*/
-const handleParsingNestedValues = (form: BodyData, key: string, value: BodyDataValue): void => {
+const handleParsingNestedValues = (
+ form: BodyData,
+ key: string,
+ value: BodyDataValue>
+): void => {
let nestedForm = form
const keys = key.split('.')
@@ -169,7 +219,7 @@ const handleParsingNestedValues = (form: BodyData, key: string, value: BodyDataV
) {
nestedForm[key] = Object.create(null)
}
- nestedForm = nestedForm[key] as BodyData
+ nestedForm = nestedForm[key] as unknown as BodyData
}
})
}
diff --git a/src/validator/validator.ts b/src/validator/validator.ts
index 4b4f1b871..cf66d0e35 100644
--- a/src/validator/validator.ts
+++ b/src/validator/validator.ts
@@ -91,7 +91,7 @@ export const validator = <
try {
const arrayBuffer = await c.req.arrayBuffer()
const formData = await bufferToFormData(arrayBuffer, contentType)
- const form: BodyData = {}
+ const form: BodyData<{ all: true }> = {}
formData.forEach((value, key) => {
if (key.endsWith('[]')) {
if (form[key] === undefined) {