Inertia Rails TypeScript Setup
Type-safe shared props, flash, and errors using InertiaConfig module augmentation.
Works identically across React, Vue, and Svelte — the globals.d.ts and InertiaConfig setup is the same for all frameworks.
Before adding TypeScript types, ask:
- Shared props (auth, flash)? → Update
SharedProps/FlashDatainindex.ts— InertiaConfig inglobals.d.tspropagates them globally viausePage() - Page-specific props? →
type Props = { ... }in the page file only — never include shared props here
InertiaConfig Module Augmentation
Define shared props type ONCE globally — never in individual page components.
InertiaConfig property names are EXACT — do not rename them:
sharedPageProps(NOT sharedProps)flashDataType(NOT flashProps, NOT flashData)errorValueType(NOT errorBag, NOT errorType)
// app/frontend/types/globals.d.ts
import type { FlashData, SharedProps } from '@/types'
declare module '@inertiajs/core' {
export interface InertiaConfig {
sharedPageProps: SharedProps // EXACT name — auto-typed for usePage().props
flashDataType: FlashData // EXACT name — auto-typed for usePage().flash
errorValueType: string[] // EXACT name — errors are arrays of strings
}
}
// app/frontend/types/index.ts
export interface FlashData {
notice?: string
alert?: string
}
export interface SharedProps {
auth: { user?: { id: number; name: string; email: string } }
}
Convention: Use auth: { user: ... } as the shared props key — this matches the
Rails inertia_share community convention ({ auth: { user: current_user } }).
The auth namespace separates authentication data from page props, preventing
collisions when a page has its own user prop. Do NOT use current_user: or
user: as top-level keys — they collide with page-specific props and break the
convention that other Inertia skills and examples assume.
BAD vs GOOD Patterns
// BAD — passing shared props as generics:
// usePage<{ users: User[], auth: AuthData, flash: FlashData }>()
// BAD — extending a SharedProps interface into page props:
// interface Props extends SharedData { users: User[] }
// BAD — declaring PageProps interface:
// interface PageProps { auth: AuthData; flash: FlashData }
// BAD — using current_user or user as top-level shared key:
// interface SharedProps { current_user: User }
// BAD — destructuring auth directly from usePage() (TS2339: 'auth' does not exist on Page):
// const { auth } = usePage()
// usePage() returns a Page object with { props, flash, component, url, ... }
// auth lives inside props, not on the Page itself
// BAD — duplicating InertiaConfig in index.ts (it belongs in globals.d.ts):
// declare module '@inertiajs/core' { ... } ← in index.ts
// GOOD — props from usePage().props, flash from usePage().flash:
const { props, flash } = usePage()
// props.auth is typed (from SharedProps via InertiaConfig)
// flash.notice is typed (from FlashData via InertiaConfig)
Important: globals.d.ts configures InertiaConfig ONCE. When adding a new shared
prop, only update index.ts — do NOT touch globals.d.ts:
// BEFORE — app/frontend/types/index.ts
export interface SharedProps {
auth: { user?: { id: number; name: string; email: string } }
}
// AFTER — add the new key here, NOT in globals.d.ts
export interface SharedProps {
auth: { user?: { id: number; name: string; email: string } }
notifications: { unread_count: number }
}
InertiaConfig in globals.d.ts references SharedProps by name — it picks up the
change automatically. Adding a second declare module '@inertiajs/core' causes conflicts.
Page-Specific Props
Page components type ONLY their own props. Shared props (like auth) and flash come from InertiaConfig automatically.
type vs interface for page props (React-specific)
This constraint applies to React only. Vue's defineProps<T>() and Svelte's
$props() do not use usePage<T>() generics, so interface works fine there.
usePage<T>() requires T to have an index signature. type aliases have one
implicitly; interface declarations do not. Using interface with usePage
causes TS2344 at compile time.
| Pattern | Works with usePage<T>()? | Notes |
|---|---|---|
type Props = { users: User[] } | Yes | Preferred — just works |
interface Props { users: User[] } | No — TS2344 | Missing index signature |
usePage<Required<Props>>() | Yes | Wraps interface to add index signature |
// React
type Props = {
users: User[] // page-specific only
// auth is NOT here — it comes from InertiaConfig globally
}
export default function Index({ users }: Props) {
// Access shared props separately:
const { props, flash } = usePage()
// props.auth is typed via InertiaConfig
// flash.notice is typed via InertiaConfig
return <UserList users={users} />
}
Accessing shared props in Vue and Svelte
Vue and Svelte use different patterns to access shared props, but InertiaConfig typing works the same way.
<!-- Vue 3 — usePage() returns reactive object; use computed() for derived values -->
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
const page = usePage()
const userName = computed(() => page.props.auth.user?.name) // typed via InertiaConfig
</script>
<!-- Svelte — page store from @inertiajs/svelte -->
<script lang="ts">
import { page } from '@inertiajs/svelte'
// $page.props.auth is typed via InertiaConfig
// $page.flash.notice is typed via InertiaConfig
</script>
Common TypeScript Errors
| Error | Cause | Fix |
|---|---|---|
TS2344 on usePage<Props>() | interface lacks index signature | Use type Props = { ... } instead of interface, or wrap: usePage<Required<Props>>() |
TS2339 'auth' does not exist on type Page | Destructuring auth from usePage() directly | usePage() returns { props, flash, ... } — use usePage().props.auth, not usePage().auth |
TS2339 'flash' does not exist on type | Accessing usePage().props.flash | Flash is top-level: usePage().flash, NOT usePage().props.flash |
| Shared props untyped | Missing InertiaConfig | Add globals.d.ts with module augmentation (see above) |
| InertiaConfig not taking effect | Declaration in wrong file | Must be in a .d.ts file (e.g., globals.d.ts), not in .ts — TypeScript ignores declare module in regular .ts files that have imports/exports |
| Types correct but IDE shows errors | globals.d.ts not included | Verify tsconfig.app.json includes the types directory in include array |
Typelizer Integration
If using the typelizer gem (see alba-inertia skill), SharedProps are auto-generated
from your serializer — do NOT manually write the SharedProps interface in index.ts.
You only write globals.d.ts once (the InertiaConfig augmentation). When you add a new
attribute to SharedPropsResource, Typelizer regenerates index.ts and the types
propagate via InertiaConfig — no manual type updates needed.
Related Skills
- Shared props setup →
inertia-rails-controllers(inertia_share) - Flash config →
inertia-rails-controllers(flash_keys) - Auto-generated types →
alba-inertia(Typelizer + Alba resources) - Page component props →
inertia-rails-pages(type Props pattern)