TypeScript in Laravel Projects: Complete Setup Guide 2026

calendar_today July 20, 2026
person info@softcrony.com
folder DevOps

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