From 5cf65c6f697eb4db9d3fe3562ff879b0663e4532 Mon Sep 17 00:00:00 2001 From: nkfr26 Date: Thu, 20 Aug 2026 00:26:28 +0900 Subject: [PATCH 1/5] fix(inertia): normalize empty props in PageProps --- .changeset/fast-kings-reply.md | 5 +++ packages/inertia/src/page-props.test.ts | 43 +++++++++++++++++++++++++ packages/inertia/src/page-props.ts | 20 +++++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .changeset/fast-kings-reply.md create mode 100644 packages/inertia/src/page-props.test.ts diff --git a/.changeset/fast-kings-reply.md b/.changeset/fast-kings-reply.md new file mode 100644 index 000000000..06aea2c4a --- /dev/null +++ b/.changeset/fast-kings-reply.md @@ -0,0 +1,5 @@ +--- +'@hono/inertia': patch +--- + +Normalize the props resolved by `PageProps`: renders without props now resolve to `{}`, and when the same page is rendered with and without props by different handlers, the props are made optional. \ No newline at end of file diff --git a/packages/inertia/src/page-props.test.ts b/packages/inertia/src/page-props.test.ts new file mode 100644 index 000000000..6e93c0a89 --- /dev/null +++ b/packages/inertia/src/page-props.test.ts @@ -0,0 +1,43 @@ +import { Hono } from 'hono' +import { describe, expectTypeOf, it } from 'vitest' +import type { PageProps } from './page-props' +import { inertia } from './index' + +const _app = new Hono() + .use(inertia()) + .get('/with-props', (c) => c.render('WithProps', { lazy: () => Promise.resolve({ id: 0 }) })) + .get('/without-props', (c) => c.render('WithoutProps')) + .post( + '/optional-props', + (c) => c.render('OptionalProps', { errors: { email: 'invalid' } }), + (c) => c.render('OptionalProps') + ) + .get( + '/union-props', + (c) => c.render('UnionProps', { a: 0 }), + (c) => c.render('UnionProps', { b: 'string' }) + ) + +declare module '@hono/inertia' { + interface AppRegistry { + app: typeof _app + } +} + +describe('PageProps', () => { + it('resolves lazy prop values', () => { + expectTypeOf>().toEqualTypeOf<{ lazy: { id: number } }>() + }) + + it('collapses the props of a render without props to {}', () => { + expectTypeOf>().toEqualTypeOf<{}>() + }) + + it('makes the props optional when multiple handlers render the same page with and without props', () => { + expectTypeOf>().toEqualTypeOf<{ errors?: { email: string } }>() + }) + + it('keeps non-empty props from multiple handlers', () => { + expectTypeOf>().toEqualTypeOf<{ a: number } | { b: string }>() + }) +}) diff --git a/packages/inertia/src/page-props.ts b/packages/inertia/src/page-props.ts index a7140c040..27a96429b 100644 --- a/packages/inertia/src/page-props.ts +++ b/packages/inertia/src/page-props.ts @@ -49,6 +49,22 @@ type RenderOutput = : never : never +type NonEmptyProps = T extends Record ? never : T + +type NormalizeProps = [T] extends [never] + ? never + : [NonEmptyProps] extends [never] + ? {} + : [Extract>] extends [never] + ? NonEmptyProps + : Partial> + +/** + * Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability. + * @copyright from sindresorhus/type-fest + */ +type Simplify = { [K in keyof T]: T[K] } & {} + /** * Resolves the props type for a given Inertia page component name. * @@ -56,4 +72,6 @@ type RenderOutput = */ export type PageProps< C extends RenderOutput['component'] = RenderOutput['component'], -> = Extract, { component: C }>['props'] +> = C extends unknown + ? Simplify, { component: C }>['props']>> + : never From cbca0d4a330669333802c3b585f23b649641d4f4 Mon Sep 17 00:00:00 2001 From: nkfr26 Date: Thu, 20 Aug 2026 21:06:28 +0900 Subject: [PATCH 2/5] refactor(inertia): distribute output extraction over handler unions --- packages/inertia/src/page-props.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/inertia/src/page-props.ts b/packages/inertia/src/page-props.ts index 27a96429b..90f9e6e40 100644 --- a/packages/inertia/src/page-props.ts +++ b/packages/inertia/src/page-props.ts @@ -28,16 +28,14 @@ type RegisteredApp = AppRegistry extends { app: infer A } ? A : never type Distribute = T extends infer U ? U : never +type MethodOutput = MethodSchema extends { output: infer O } ? Distribute : never + type AllOutputs = Distribute< { [Path in keyof ExtractSchema & string]: { - [ - Method in keyof ExtractSchema[Path] & string - ]: ExtractSchema[Path][Method] extends { - output: infer O - } - ? Distribute - : never + [Method in keyof ExtractSchema[Path] & string]: MethodOutput< + ExtractSchema[Path][Method] + > }[keyof ExtractSchema[Path] & string] }[keyof ExtractSchema & string] > From a30b90b5507fb9969ddbba0b60a8b8f4068cc15f Mon Sep 17 00:00:00 2001 From: nkfr26 Date: Fri, 21 Aug 2026 03:50:32 +0900 Subject: [PATCH 3/5] refactor(inertia): drop redundant NonEmptyProps in NormalizeProps --- packages/inertia/src/page-props.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/inertia/src/page-props.ts b/packages/inertia/src/page-props.ts index 90f9e6e40..5849c4b24 100644 --- a/packages/inertia/src/page-props.ts +++ b/packages/inertia/src/page-props.ts @@ -54,7 +54,7 @@ type NormalizeProps = [T] extends [never] : [NonEmptyProps] extends [never] ? {} : [Extract>] extends [never] - ? NonEmptyProps + ? T : Partial> /** From 888201386b9c36d8a2336cb112afd162683361f0 Mon Sep 17 00:00:00 2001 From: nkfr26 Date: Tue, 25 Aug 2026 23:13:58 +0900 Subject: [PATCH 4/5] fix(inertia): normalize props-less renders as optional never keys --- .changeset/fast-kings-reply.md | 2 +- packages/inertia/src/page-props.test.ts | 30 +++++++++++-------------- packages/inertia/src/page-props.ts | 18 +++++++-------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.changeset/fast-kings-reply.md b/.changeset/fast-kings-reply.md index 06aea2c4a..a5d7092ac 100644 --- a/.changeset/fast-kings-reply.md +++ b/.changeset/fast-kings-reply.md @@ -2,4 +2,4 @@ '@hono/inertia': patch --- -Normalize the props resolved by `PageProps`: renders without props now resolve to `{}`, and when the same page is rendered with and without props by different handlers, the props are made optional. \ No newline at end of file +Normalize the props resolved by `PageProps`: renders without props now resolve to `{}`, and when the same page is rendered with and without props by different handlers, the props-less variant shares the other renders' keys as optional `never`, so absent props must be accessed with `?.`. diff --git a/packages/inertia/src/page-props.test.ts b/packages/inertia/src/page-props.test.ts index 6e93c0a89..a390fe022 100644 --- a/packages/inertia/src/page-props.test.ts +++ b/packages/inertia/src/page-props.test.ts @@ -5,17 +5,13 @@ import { inertia } from './index' const _app = new Hono() .use(inertia()) - .get('/with-props', (c) => c.render('WithProps', { lazy: () => Promise.resolve({ id: 0 }) })) + .get('/lazy-props', (c) => c.render('LazyProps', { lazy: () => Promise.resolve({ id: 0 }) })) .get('/without-props', (c) => c.render('WithoutProps')) - .post( - '/optional-props', - (c) => c.render('OptionalProps', { errors: { email: 'invalid' } }), - (c) => c.render('OptionalProps') - ) .get( '/union-props', - (c) => c.render('UnionProps', { a: 0 }), - (c) => c.render('UnionProps', { b: 'string' }) + (c) => c.render('UnionProps'), + (c) => c.render('UnionProps', { kind: 'ok' as const, value: 0 }), + (c) => c.render('UnionProps', { kind: 'ng' as const, error: 'message' }) ) declare module '@hono/inertia' { @@ -25,19 +21,19 @@ declare module '@hono/inertia' { } describe('PageProps', () => { - it('resolves lazy prop values', () => { - expectTypeOf>().toEqualTypeOf<{ lazy: { id: number } }>() + it('resolves lazy props', () => { + expectTypeOf>().toEqualTypeOf<{ lazy: { id: number } }>() }) - it('collapses the props of a render without props to {}', () => { + it('normalizes renders without props to {}', () => { expectTypeOf>().toEqualTypeOf<{}>() }) - it('makes the props optional when multiple handlers render the same page with and without props', () => { - expectTypeOf>().toEqualTypeOf<{ errors?: { email: string } }>() - }) - - it('keeps non-empty props from multiple handlers', () => { - expectTypeOf>().toEqualTypeOf<{ a: number } | { b: string }>() + it('normalizes union types', () => { + expectTypeOf>().toEqualTypeOf< + | { kind: 'ok'; value: number } + | { kind: 'ng'; error: string } + | { kind?: never; value?: never; error?: never } + >() }) }) diff --git a/packages/inertia/src/page-props.ts b/packages/inertia/src/page-props.ts index 5849c4b24..09bbee5a4 100644 --- a/packages/inertia/src/page-props.ts +++ b/packages/inertia/src/page-props.ts @@ -47,18 +47,18 @@ type RenderOutput = : never : never -type NonEmptyProps = T extends Record ? never : T +type AllKeys = T extends unknown ? keyof T : never -type NormalizeProps = [T] extends [never] - ? never - : [NonEmptyProps] extends [never] - ? {} - : [Extract>] extends [never] - ? T - : Partial> +/** + * Props-less renders are normalized to the other renders' keys as optional `never`, + * so access keeps working while the value may still be `undefined`. + */ +type NormalizeProps = T extends Record + ? { [K in AllKeys>>]?: never } + : T /** - * Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability. + * Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aid with assignability. * @copyright from sindresorhus/type-fest */ type Simplify = { [K in keyof T]: T[K] } & {} From 3ae670eb287d05e21ec5e6e5510b832c5eb3acf6 Mon Sep 17 00:00:00 2001 From: nkfr26 Date: Wed, 26 Aug 2026 10:06:00 +0900 Subject: [PATCH 5/5] chore(inertia): format --- packages/inertia/src/page-props.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/inertia/src/page-props.ts b/packages/inertia/src/page-props.ts index 09bbee5a4..19161bd07 100644 --- a/packages/inertia/src/page-props.ts +++ b/packages/inertia/src/page-props.ts @@ -53,9 +53,10 @@ type AllKeys = T extends unknown ? keyof T : never * Props-less renders are normalized to the other renders' keys as optional `never`, * so access keeps working while the value may still be `undefined`. */ -type NormalizeProps = T extends Record - ? { [K in AllKeys>>]?: never } - : T +type NormalizeProps = + T extends Record + ? { [K in AllKeys>>]?: never } + : T /** * Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aid with assignability.