Type safety
Fondu checks the seams between the server and the browser at compile time: what a route sends to a page, the URLs your pages link to, the input your handlers read. Rename a prop, move a route or change a schema, and TypeScript points at every place that no longer fits, before anything runs.
Where the types come from
There is nothing to declare twice. Each type has one source, and the rest follows from it:
| Source | Checks |
|---|---|
| a page component's props | render(c, 'page', props): the page exists, every prop is there and fits |
the files in app/routes/ | href('/users/:id', { id }): the URL exists, its params, its actions |
a handler's schemas (withValidator) | the body, query and params the handler receives |
| declarations next to your middleware | share(), flash(), usePage(), c.get() |
The dev server and the build write the first two into app/fondu.gen.d.ts as your files change; commit it.
Page props
The component is the source: its parameter's type is what render() takes for that page.
// app/pages/users/show.tsx
export default function Show({ user, canEdit }: { user: User; canEdit: boolean }) { … }// app/routes/users/[id].ts
render(c, 'users/show', { user }) // error: canEdit is missing
render(c, 'users/sho', { user, canEdit }) // error: no such page
render(c, 'users/show', { user, canEdit: () => isOwner(c, user) }) // a function returning the prop's type: fineThe kinds of props keep the type of their value: defer(() => stats()) fits a prop typed like stats()'s result.
URLs
href() from fondu/client takes the URL patterns of app/routes/, and a route's actions as ?name:
href('/users/:id', { id: 7 }) // '/users/7'
href('/users/:id?rename', { id: 7 }) // '/users/7?rename': actions.rename
href('/users/:id') // error: the param is missing
href('/user/:id', { id: 7 }) // error: no such routeHandler input
defineHandler.withValidator() types the handler's input from its schemas, as they parse it: with
z.coerce.number(), params.id is a number.
const params = z.object({ id: z.coerce.number() })
export const loader = defineHandler.withValidator({ params })(async (c, { params }) => {
params.id // number
})Any Standard Schema library works. Export the schema from a file that isn't server code and the page can check the same rules before sending: Forms and validation.
Shared props, flash data and context values
What a middleware sets is typed where it is set, by declaring Hono's and Inertia's own interfaces:
// app/routes/_middleware.ts
declare module 'hono' {
interface ContextVariableMap {
user: User | null // c.set('user', …), c.get('user')
}
}
declare module '@inertiajs/core' {
interface InertiaConfig {
sharedPageProps: { auth: { user: { name: string } | null } } // share(), usePage().props
flashDataType: { message?: string } // flash(), usePage().flash
}
}share() and flash() then take only those, and usePage() returns them. A page prop named like a shared prop but of
another type is an error, as it would hide the shared one from the layout.
What crosses as JSON
Props travel to the browser as JSON. Type a page's props as the page receives them: a Date arrives as a string, a
Map as {}, and undefined fields are left out. Send createdAt.toISOString() for a prop typed string.
Setup
None beyond tsconfig.json: a new app has it. Keep app/fondu.gen.d.ts committed, so a fresh clone or CI has the
types before the first build.