Skip to content

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 (or app_path() when not set) for a class extending Inertia\Middleware.
  • It statically analyzes that middleware's share(Request $request): array method — 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 via inertia.augmentation_filename).
  • If no Inertia\Middleware subclass is found, or it returns no shared data, no file is generated.

Anatomy of the Generated File

Given this middleware:

php
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:

typescript
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 = ...; } } makes Inertia.SharedData available by bare name in any .ts file 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 own usePage<T>() / shared-data typing so usePage().props is typed correctly throughout your frontend, without you writing that augmentation by hand.
  • errorValueType: string[] is only added when the middleware has a protected $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 a declare global block inside a file that's an ES module (i.e., has at least one top-level import or export). Without it, the declare global block would be silently ignored.

Type Resolution Priority

Each key returned from share() resolves to a TypeScript type using this priority order (highest wins):

  1. #[TsCasts] on the middleware class or its share() method — the same attribute used by models, resources, and broadcast events.
  2. @return array{...} PHPDoc on share() — 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).
  3. Surveyor's static inference — the default, covering plain values, nested arrays, conditionals, and spreads.
php
#[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:

typescript
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:

php
// 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)),
]);
typescript
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.

php
$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:

  1. inertia.output_directory, if set.
  2. routes.output_directory, if set — since page-prop types generated per-route (see Inertia Integration) reference Inertia.SharedData, keeping the augmentation file alongside routes by default means both live in a predictable, related location.
  3. 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.

Released under the MIT License.