Inertia
The Laravel TypeScript Publisher analyzes your HandleInertiaRequests middleware's share() method and generates inertia-config.d.ts — a module augmentation for @inertiajs/core plus a global Inertia.SharedData type. Every Inertia page component gets fully-typed shared props automatically, without hand-maintaining a separate type.
This page covers the shared-data analysis and module augmentation file. For per-route page-prop types (the component field and annotatePageProps threading on individual routes), see Inertia Integration in the Routing docs — that's a related but separate piece of the pipeline.
How the Augmentation File Is Generated
- The package searches
inertia.inertia_middleware_path(orapp_path()when not set) for a class extendingInertia\Middleware. - It statically analyzes that middleware's
share(Request $request): arraymethod — via Surveyor — resolving every key's value to a TypeScript type without running the application. - The result is rendered into
inertia-config.d.ts(filename configurable viainertia.augmentation_filename). - If no
Inertia\Middlewaresubclass is found, or it returns no shared data, no file is generated.
Anatomy of the Generated File
Given this middleware:
class HandleInertiaRequests extends Middleware
{
protected $withAllErrors = true;
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => ['user' => $request->user()],
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
],
'appName' => config('app.name'),
];
}
}The package generates inertia-config.d.ts:
declare global {
namespace Inertia {
type SharedData = {
auth: { user: { id: number; name: string; email: string } | null };
flash: { success: string | null; error: string | null };
appName: string;
};
}
}
declare module "@inertiajs/core" {
export interface InertiaConfig {
sharedPageProps: {
auth: { user: { id: number; name: string; email: string } | null };
flash: { success: string | null; error: string | null };
appName: string;
};
errorValueType: string[];
}
}
export {};declare global { namespace Inertia { type SharedData = ...; } }makesInertia.SharedDataavailable by bare name in any.tsfile in your project — including generated controller files that intersect it with page-specific props (see Inertia Integration).declare module '@inertiajs/core' { ... InertiaConfig ... }augments Inertia's ownusePage<T>()/ shared-data typing sousePage().propsis typed correctly throughout your frontend, without you writing that augmentation by hand.errorValueType: string[]is only added when the middleware has aprotected $withAllErrors = true;property — it matches the shape Inertia uses for its validation error bag in that mode.export {};at the end is required — TypeScript only processes adeclare globalblock inside a file that's an ES module (i.e., has at least one top-levelimportorexport). Without it, thedeclare globalblock would be silently ignored.
Type Resolution Priority
Each key returned from share() resolves to a TypeScript type using this priority order (highest wins):
#[TsCasts]on the middleware class or itsshare()method — the same attribute used by models, resources, and broadcast events.@return array{...}PHPDoc onshare()— a manually-written shape annotation, useful when a key's value can't be statically inferred (e.g. it comes from a closure or a method call Surveyor can't resolve).- Surveyor's static inference — the default, covering plain values, nested arrays, conditionals, and spreads.
#[TsCasts(['appName' => 'string'])]
class HandleInertiaRequests extends Middleware
{
/**
* @return array{flash: array{success: string|null, error: string|null}}
*/
public function share(Request $request): array
{
return [
...parent::share($request),
'flash' => $this->resolveFlashMessages($request), // opaque method call
];
}
}Here, appName's type comes from #[TsCasts], flash's type comes from the @return docblock (since resolveFlashMessages() isn't itself analyzed), and every other key (like auth, from ...parent::share($request)) falls back to Surveyor's own inference.
Preserve-Keys Resource Collections in Page Props
This is about per-route page props (see Inertia Integration), not share() — noted here because it's the same paginated-collection typing this page's other sections describe.
A ResourceCollection (or a resource collected via Resource::collection()) that opts into Laravel's key-preserving behavior — the #[PreserveKeys] attribute or the older public $preserveKeys = true; property — serializes its data as a JSON object keyed by the source collection's own keys, not a JSON array. A paginated page prop backed by such a collection types its data member to match:
import type { JsonResourcePaginator } from "@tolki/types";
// $wrap = null (flat) or Resource::collection($paginator) on a preserve-keys resource:
export type TeamsPageProps = Inertia.SharedData & {
teams: Omit<JsonResourcePaginator<Team>, "data"> & {
data: Record<string, Team>;
};
};JsonResourcePaginator<T>'s own data is T[] (see API Resources § Pagination), so a key-preserving collection can't use it unmodified — the page prop type Omits the array-typed data and replaces it with a keyed Record<string, T>.
A named, non-flat collection (new TeamCollection($paginator), wrapped in a data key) doesn't need this rewrite at all: its page prop already references the collection's own generated interface (TeamCollection & ResourcePagination), and that interface's data member is generated as Record<string, T> directly whenever the collection preserves keys — paginated or not. Only the two shapes that would otherwise degrade to a paginator utility type with an array-typed data — the flat collection and the anonymous Resource::collection() case — need the Omit<...> & { data: Record<...> } rewrite.
Paginating Inline in the Render Call
A paginator does not have to be assigned to a variable first. Both of these produce the same page-prop type:
// Via an intermediate variable
$teams = Team::query()->paginate(10);
return Inertia::render('Teams/Index', [
'teams' => new TeamCollection($teams),
]);
// Inline, with no intermediate variable
return Inertia::render('Teams/Index', [
'teams' => new TeamCollection(Team::query()->paginate(10)),
]);export type IndexPageProps = Inertia.SharedData & {
teams: TeamCollection & ResourcePagination;
};paginate(), simplePaginate(), and cursorPaginate() are all recognized, in both the new SomeCollection(...) and SomeResource::collection(...) forms.
WARNING
An unrecognized paginator does not produce a missing type — it produces a wrong one. The analyzer defaults an unresolved prop to "not paginated", so the prop still gets a type from the ordinary resource/collection analysis, just without the pagination wrapper. Before inline detection, the second form above typed as a bare TeamCollection, silently omitting ResourcePagination.
One form is still not followed: a query builder assigned to a variable before the paginator call. The chain has to reach a static call on the model directly.
$q = Post::query();
return Inertia::render('Posts/Index', [
'posts' => new PostCollection($q->paginate(10)), // not detected
]);Spread Support (...parent::share($request))
The base Inertia\Middleware::share() method's own return type (Laravel's default errors/errors_bag keys, plus anything your parent middleware layers add) is included automatically when your override spreads it in — same as trait/parent spreading elsewhere in the package.
Output Location
The augmentation file's output directory is resolved with this priority:
inertia.output_directory, if set.routes.output_directory, if set — since page-prop types generated per-route (see Inertia Integration) referenceInertia.SharedData, keeping the augmentation file alongside routes by default means both live in a predictable, related location.- The global
output_directory.
Configuration Reference
The full list of inertia.* config keys — including component_casing and ui_table_package, which apply to the related per-route page-props feature — lives in the Configuration Reference.