Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-kings-reply.md
Original file line number Diff line number Diff line change
@@ -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 `?.`.
39 changes: 39 additions & 0 deletions packages/inertia/src/page-props.test.ts
Original file line number Diff line number Diff line change
@@ -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<PageProps<'LazyProps'>>().toEqualTypeOf<{ lazy: { id: number } }>()
})

it('normalizes renders without props to {}', () => {
expectTypeOf<PageProps<'WithoutProps'>>().toEqualTypeOf<{}>()
})

it('normalizes union types', () => {
expectTypeOf<PageProps<'UnionProps'>>().toEqualTypeOf<
| { kind: 'ok'; value: number }
| { kind: 'ng'; error: string }
| { kind?: never; value?: never; error?: never }
>()
})
})
33 changes: 25 additions & 8 deletions packages/inertia/src/page-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,14 @@ type RegisteredApp = AppRegistry extends { app: infer A } ? A : never

type Distribute<T> = T extends infer U ? U : never

type MethodOutput<MethodSchema> = MethodSchema extends { output: infer O } ? Distribute<O> : never

type AllOutputs<App> = Distribute<
{
[Path in keyof ExtractSchema<App> & string]: {
[
Method in keyof ExtractSchema<App>[Path] & string
]: ExtractSchema<App>[Path][Method] extends {
output: infer O
}
? Distribute<O>
: never
[Method in keyof ExtractSchema<App>[Path] & string]: MethodOutput<
ExtractSchema<App>[Path][Method]
>
}[keyof ExtractSchema<App>[Path] & string]
}[keyof ExtractSchema<App> & string]
>
Expand All @@ -49,11 +47,30 @@ type RenderOutput<App> =
: never
: never

type AllKeys<T> = 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, All = T> =
T extends Record<string, never>
? { [K in AllKeys<Exclude<All, Record<string, never>>>]?: 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<T> = { [K in keyof T]: T[K] } & {}

/**
* Resolves the props type for a given Inertia page component name.
*
* Requires {@link AppRegistry} to be augmented with the Hono app type.
*/
export type PageProps<
C extends RenderOutput<RegisteredApp>['component'] = RenderOutput<RegisteredApp>['component'],
> = Extract<RenderOutput<RegisteredApp>, { component: C }>['props']
> = C extends unknown
? Simplify<NormalizeProps<Extract<RenderOutput<RegisteredApp>, { component: C }>['props']>>
: never