diff --git a/.changeset/fast-kings-reply.md b/.changeset/fast-kings-reply.md new file mode 100644 index 000000000..a5d7092ac --- /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-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 new file mode 100644 index 000000000..a390fe022 --- /dev/null +++ b/packages/inertia/src/page-props.test.ts @@ -0,0 +1,39 @@ +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('/lazy-props', (c) => c.render('LazyProps', { lazy: () => Promise.resolve({ id: 0 }) })) + .get('/without-props', (c) => c.render('WithoutProps')) + .get( + '/union-props', + (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' { + interface AppRegistry { + app: typeof _app + } +} + +describe('PageProps', () => { + it('resolves lazy props', () => { + expectTypeOf>().toEqualTypeOf<{ lazy: { id: number } }>() + }) + + it('normalizes renders without props to {}', () => { + expectTypeOf>().toEqualTypeOf<{}>() + }) + + 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 a7140c040..19161bd07 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] > @@ -49,6 +47,23 @@ type RenderOutput = : never : never +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 + +/** + * 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] } & {} + /** * Resolves the props type for a given Inertia page component name. * @@ -56,4 +71,6 @@ type RenderOutput = */ export type PageProps< C extends RenderOutput['component'] = RenderOutput['component'], -> = Extract, { component: C }>['props'] +> = C extends unknown + ? Simplify, { component: C }>['props']>> + : never