TypeScript has become the professional baseline for JavaScript development in 2026. If your Laravel project has a React or Vue frontend, running plain JavaScript is leaving a significant safety net on the table.
This guide covers a complete TypeScript setup for Laravel — from Vite configuration to shared types between your PHP backend and React frontend.
Why TypeScript in a Laravel Project?
The specific benefits for Laravel + React projects:
- Catch prop mismatches at compile time — when your Laravel controller changes a response shape, TypeScript tells you immediately which React components broke
- Autocomplete for API responses — type your API response shapes and get full IDE autocomplete on every property
- Safer refactoring — rename a field in your type definition and TypeScript finds every usage across all components
- Better team collaboration — types are documentation that the compiler enforces
Step 1 — Install TypeScript
npm install --save-dev typescript @types/node @types/react @types/react-dom
// tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext", "DOM"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["resources/js/*"]
},
"skipLibCheck": true
},
"include": [
"resources/js/**/*.ts",
"resources/js/**/*.tsx",
"resources/js/**/*.d.ts"
],
"exclude": ["node_modules", "public"]
}
Step 2 — Update Vite Config
// vite.config.ts
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [
laravel({
input: ['resources/js/app.tsx'], // .tsx extension
refresh: true,
}),
react(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './resources/js'),
},
},
});
Step 3 — Type Your Laravel API Responses
Create a resources/js/types directory for all shared types:
// resources/js/types/models.ts
export interface User {
id: number;
name: string;
email: string;
created_at: string;
updated_at: string;
}
export interface Project {
id: number;
name: string;
description: string | null;
status: 'active' | 'paused' | 'completed' | 'cancelled';
user_id: number;
created_at: string;
updated_at: string;
// Relationships (optional — only present when eager loaded)
tasks?: Task[];
owner?: User;
}
export interface Task {
id: number;
project_id: number;
title: string;
description: string | null;
status: 'pending' | 'in_progress' | 'completed';
priority: 'low' | 'medium' | 'high' | 'urgent';
due_date: string | null;
completed_at: string | null;
created_at: string;
updated_at: string;
}
// Pagination wrapper
export interface Paginated<T> {
data: T[];
current_page: number;
last_page: number;
per_page: number;
total: number;
from: number | null;
to: number | null;
links: Array<{
url: string | null;
label: string;
active: boolean;
}>;
}
// resources/js/types/inertia.d.ts
// Type Inertia page props globally
import { User } from './models';
declare module '@inertiajs/react' {
interface PageProps {
auth: {
user: User;
};
flash: {
success?: string;
error?: string;
};
}
}
Step 4 — Type Inertia Page Components
// resources/js/Pages/Projects/Index.tsx
import { Head, Link } from '@inertiajs/react';
import type { PageProps } from '@inertiajs/react';
import type { Project, Paginated } from '@/types/models';
import AppLayout from '@/Layouts/AppLayout';
// Typed page props
interface Props extends PageProps {
projects: Paginated<Project>;
filters: {
search?: string;
status?: string;
};
}
export default function ProjectsIndex({ projects, filters }: Props) {
return (
<AppLayout>
<Head title="Projects" />
<div>
{projects.data.map((project) => (
<Link key={project.id} href={`/projects/${project.id}`}>
<div>
<h3>{project.name}</h3>
<span>{project.status}</span>
{/* TypeScript knows project.tasks is Task[] | undefined */}
{project.tasks && (
<span>{project.tasks.length} tasks</span>
)}
</div>
</Link>
))}
</div>
<div>
Page {projects.current_page} of {projects.last_page}
</div>
</AppLayout>
);
}
Step 5 — Typed Forms with Inertia
// resources/js/Pages/Projects/Create.tsx
import { useForm } from '@inertiajs/react';
interface CreateProjectForm {
name: string;
description: string;
status: 'active' | 'paused';
}
export default function CreateProject() {
const { data, setData, post, processing, errors } = useForm<CreateProjectForm>({
name: '',
description: '',
status: 'active',
});
// TypeScript knows data.name is string
// TypeScript knows data.status is 'active' | 'paused' — not any string
// TypeScript errors if you try to set data.unknown_field
return (
<form onSubmit={(e) => { e.preventDefault(); post('/projects'); }}>
<input
value={data.name}
onChange={(e) => setData('name', e.target.value)}
/>
{errors.name && <p>{errors.name}</p>}
<select
value={data.status}
onChange={(e) => setData('status', e.target.value as CreateProjectForm['status'])}
>
<option value="active">Active</option>
<option value="paused">Paused</option>
</select>
<button type="submit" disabled={processing}>Create</button>
</form>
);
}
Step 6 — Typed Routes with Ziggy
Ziggy generates JavaScript route helpers from your Laravel routes — and with TypeScript, you get full type safety on route parameters:
composer require tightenco/ziggy
npm install --save-dev ziggy-js
// routes/web.php — add names to all routes
Route::get('/projects', [ProjectController::class, 'index'])->name('projects.index');
Route::get('/projects/{project}', [ProjectController::class, 'show'])->name('projects.show');
Route::post('/projects', [ProjectController::class, 'store'])->name('projects.store');
// Generate TypeScript types from your routes
php artisan ziggy:generate --types
// resources/js/Pages/ProjectShow.tsx
import { Link } from '@inertiajs/react';
import { route } from 'ziggy-js';
export default function ProjectShow({ project }) {
return (
<div>
{/* TypeScript knows projects.show requires {project: number} */}
<Link href={route('projects.show', { project: project.id })}>
View Project
</Link>
{/* TypeScript ERROR: missing required parameter */}
{/* <Link href={route('projects.show')}> */}
</div>
);
}
Step 7 — Laravel Data for Shared Types (Advanced)
The spatie/laravel-data package can automatically generate TypeScript types from your PHP Data classes — eliminating manual type duplication:
composer require spatie/laravel-data
npm install --save-dev @spatie/laravel-typescript-transformer
<?php
// app/Data/ProjectData.php
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
#[TypeScript]
class ProjectData extends Data
{
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly ?string $description,
public readonly ProjectStatus $status, // enum
public readonly Carbon $created_at,
) {}
public static function fromModel(Project $project): self
{
return new self(
id: $project->id,
name: $project->name,
description: $project->description,
status: $project->status,
created_at: $project->created_at,
);
}
}
# Generate TypeScript types from PHP Data classes
php artisan typescript:transform
This generates:
// auto-generated — do not edit
export type ProjectData = {
id: number;
name: string;
description: string | null;
status: 'active' | 'paused' | 'completed' | 'cancelled';
created_at: string;
}
Your PHP and TypeScript types stay in sync automatically — change the PHP class and regenerate.
TypeScript Strict Mode — Common Errors and Fixes
// Error: Object is possibly null
const name = user?.name; // ✅ optional chaining
if (user) { const name = user.name; } // ✅ null check
// Error: Element implicitly has 'any' type
const value = data['key']; // ❌
const value = (data as Record<string, unknown>)['key']; // ✅
// Error: Type 'string' is not assignable to status enum
setData('status', 'unknown'); // ❌ TypeScript catches this
setData('status', 'active'); // ✅
// Error: Property does not exist on type
project.unknown_field; // ❌ TypeScript catches this
project.name; // ✅
If you’re starting a new Laravel + React project and want TypeScript set up correctly from day one, or want to add TypeScript to an existing project, our frontend team at Softcrony can help.
Leave a comment