# TypeScript props on Laravel Vue pages

> defineProps on the Inertia page is the type of the controller payload. A missing key fails the check on your machine, before a blank v-for in the browser.

- URL: https://laravel-vuejs.space/typescript-laravel-vue-pages
- Author: Anass Ez-zouaine (https://laravel-vuejs.space/author/anass-ez-zouaine)
- Published: 2026-10-10
- Topics: Laravel, VueJs, TypeScript, Inertia

Your Inertia page renders an empty list and the console is silent. The controller sent `posts.data`, the template looped over `posts`. One `defineProps` type turns that silent bug into a red line before you open the browser.

> **TL;DR**
>
> -   Type the props in the page with `defineProps<{ posts: Page }>()`, matching what the controller actually returns.
> -   A paginator wraps rows in `data`; loop over `posts.data`, not `posts`.
> -   Dates are JSON strings, so type them as `string | null`.
> -   Keep the interface next to the page; run the kit’s `vue-tsc` check.
>
> **What you'll learn**
>
> -   How to type an Inertia page with `defineProps`
> -   The shape a Laravel paginator sends
> -   Why dates arrive as strings
> -   Where to keep the types
>
> **Who it's for:** Laravel + Vue developers on the TypeScript starter kit
>
> **Applies to:** Laravel 13 · Vue starter kit · Vue 3.5 · TypeScript · vue-tsc

The Vue starter kit is already TypeScript. The page is untyped until you say what the controller sends. `defineProps` on that single file is enough. You do not need a shared types package, and you do not need Vue Router. Routing stays in Laravel, which is the setup in [Inertia, a SPA, or Nuxt](https://laravel-vuejs.space/laravel-vue-in-2026).

The list this types is the [CRUD guide](https://laravel-vuejs.space/laravel-inertia-vue-crud). The first setup page returned `posts` as an array. The CRUD page wraps that array in `data`. Type the shape you actually return.

## The page

`resources/js/pages/posts/Index.vue`
```vue
<script setup lang="ts">
import { Head, Link } from "@inertiajs/vue3";

interface Post {
  id: number;
  title: string;
  excerpt: string;
  published_at: string | null;
}

interface Page {
  data: Post[];
  current_page: number;
  last_page: number;
  prev_page_url: string | null;
  next_page_url: string | null;
}

defineProps<{
  posts: Page;
  filters: { search: string };
}>();
</script>

<template>
  <Head title="Posts" />
  <article v-for="post in posts.data" :key="post.id">
    <h2>{{ post.title }}</h2>
    <Link :href="`/posts/${post.id}/edit`">Edit</Link>
  </article>
</template>
```

> **Gotcha: dates are strings**
>
> `published_at` is a string in the props because Inertia serializes dates to JSON. Typing it as `Date` will not make it a `Date`. Format it as a string, or parse it on purpose.

## The posts.data trap

`posts.data` is the row list. `v-for="post in posts"` walks the paginator’s values, the `data` array and then `current_page` and the rest, so `post.title` comes out blank. The key is the second alias, `(value, key)`. The type above makes that mistake a type error if you annotate the `v-for` source as `Post[]` and then pass `posts` instead of `posts.data`.

**Blank titles:**

`resources/js/pages/posts/Index.vue`
```vue
<article v-for="post in posts" :key="post.id">
```

**Rows:**

`resources/js/pages/posts/Index.vue`
```vue
<article v-for="post in posts.data" :key="post.id">
```

### Paginator shapes differ

| Controller method | Has `last_page` and `total`? |
| --- | --- |
| `paginate()` | Yes |
| `simplePaginate()` | No |
| `cursorPaginate()` | No (cursor URLs instead) |

A simple paginator or a cursor paginator has no `last_page`. Drop that field from `Page` when the controller uses those methods. The differences are [pagination](https://laravel-vuejs.space/laravel-vue-pagination).

## What stays local

Keep this interface next to the page that receives it. A global `PageProps` augmentation is worth it later, when every page reads the same shared `auth` user. It is the wrong first step. One wrong global type then lies to every page.

Wayfinder types the URL you visit. It does not type these props. That generator is [Wayfinder](https://laravel-vuejs.space/laravel-wayfinder-vue).

## Run the check

Run the kit’s TypeScript check. A fresh starter kit calls `vue-tsc`. The script name is whatever `package.json` already has. Do not add a second checker.

> **Break it on purpose**
>
> Change `posts.data` to `posts` in the template, then run the check:
>
> ```bash
> npx vue-tsc --noEmit
> ```
>
> Put it back and the error goes away.

> **Heads up: no as any**
>
> A red `posts.data` means the controller and the page disagree. Fix the one that is wrong. Do not silence it with `as any`.

## Where this stops

Component tests for a presentational child are [tests](https://laravel-vuejs.space/test-laravel-vue). They mount the child with a `title` string. They do not replace `defineProps` on the Inertia page.

## Recap

- `defineProps<...>()` on the page types the controller payload.
- Paginators wrap rows in `data`; simple and cursor paginators have no `last_page`.
- JSON dates are strings.
- Keep types local first; let `vue-tsc` catch disagreements.

## FAQ

**Q: Why is my Inertia v-for rendering blank rows?**

You are looping over the paginator object instead of its rows. Use `posts.data`.

**Q: Should I type Laravel dates as Date in Vue?**

No. Inertia sends JSON, so dates arrive as strings. Type them as `string | null` and parse on purpose.

**Q: Do I need a global PageProps type?**

Not at first. Keep interfaces next to the page. A global augmentation pays off later, for shared props like the `auth` user.

**Q: Does Wayfinder type my page props?**

No. Wayfinder types routes and URLs. Props are typed with `defineProps` on the page.

## Next steps

- [Paginate a Laravel list in Vue](https://laravel-vuejs.space/laravel-vue-pagination): which paginator sends what.
- [Laravel Wayfinder with Vue](https://laravel-vuejs.space/laravel-wayfinder-vue): typed routes.
- [Test a Laravel + Vue app](https://laravel-vuejs.space/test-laravel-vue): component and feature tests.
- [Vue: TypeScript with the Composition API](https://vuejs.org/guide/typescript/composition-api)

## Sources

- [Vue: Typing component props](https://vuejs.org/guide/typescript/composition-api#typing-component-props)
- [Inertia v3: TypeScript](https://inertiajs.com/docs/v3/advanced/typescript)
- [Laravel: Pagination, converting results to JSON](https://laravel.com/docs/13.x/pagination#converting-results-to-json)
