{"id":107,"date":"2026-07-20T19:30:00","date_gmt":"2026-07-20T14:00:00","guid":{"rendered":"https:\/\/softcrony.com\/blog\/?p=107"},"modified":"2026-07-20T19:30:00","modified_gmt":"2026-07-20T14:00:00","slug":"typescript-laravel-setup-guide-2026","status":"publish","type":"post","link":"https:\/\/softcrony.com\/blog\/typescript-laravel-setup-guide-2026\/","title":{"rendered":"TypeScript in Laravel Projects: Complete Setup Guide 2026"},"content":{"rendered":"<p>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.<\/p>\n<p>This guide covers a complete TypeScript setup for Laravel \u2014 from Vite configuration to shared types between your PHP backend and React frontend.<\/p>\n<h2>Why TypeScript in a Laravel Project?<\/h2>\n<p>The specific benefits for Laravel + React projects:<\/p>\n<ul>\n<li><strong>Catch prop mismatches at compile time<\/strong> \u2014 when your Laravel controller changes a response shape, TypeScript tells you immediately which React components broke<\/li>\n<li><strong>Autocomplete for API responses<\/strong> \u2014 type your API response shapes and get full IDE autocomplete on every property<\/li>\n<li><strong>Safer refactoring<\/strong> \u2014 rename a field in your type definition and TypeScript finds every usage across all components<\/li>\n<li><strong>Better team collaboration<\/strong> \u2014 types are documentation that the compiler enforces<\/li>\n<\/ul>\n<h2>Step 1 \u2014 Install TypeScript<\/h2>\n<pre><code>npm install --save-dev typescript @types\/node @types\/react @types\/react-dom<\/code><\/pre>\n<pre><code>\/\/ tsconfig.json\r\n{\r\n  \"compilerOptions\": {\r\n    \"target\": \"ESNext\",\r\n    \"lib\": [\"ESNext\", \"DOM\"],\r\n    \"module\": \"ESNext\",\r\n    \"moduleResolution\": \"bundler\",\r\n    \"jsx\": \"react-jsx\",\r\n    \"strict\": true,\r\n    \"noUncheckedIndexedAccess\": true,\r\n    \"exactOptionalPropertyTypes\": true,\r\n    \"noImplicitReturns\": true,\r\n    \"noFallthroughCasesInSwitch\": true,\r\n    \"baseUrl\": \".\",\r\n    \"paths\": {\r\n      \"@\/*\": [\"resources\/js\/*\"]\r\n    },\r\n    \"skipLibCheck\": true\r\n  },\r\n  \"include\": [\r\n    \"resources\/js\/**\/*.ts\",\r\n    \"resources\/js\/**\/*.tsx\",\r\n    \"resources\/js\/**\/*.d.ts\"\r\n  ],\r\n  \"exclude\": [\"node_modules\", \"public\"]\r\n}<\/code><\/pre>\n<h2>Step 2 \u2014 Update Vite Config<\/h2>\n<pre><code>\/\/ vite.config.ts\r\nimport { defineConfig } from 'vite';\r\nimport laravel from 'laravel-vite-plugin';\r\nimport react from '@vitejs\/plugin-react';\r\nimport path from 'path';\r\n\r\nexport default defineConfig({\r\n    plugins: [\r\n        laravel({\r\n            input: ['resources\/js\/app.tsx'], \/\/ .tsx extension\r\n            refresh: true,\r\n        }),\r\n        react(),\r\n    ],\r\n    resolve: {\r\n        alias: {\r\n            '@': path.resolve(__dirname, '.\/resources\/js'),\r\n        },\r\n    },\r\n});<\/code><\/pre>\n<h2>Step 3 \u2014 Type Your Laravel API Responses<\/h2>\n<p>Create a <code>resources\/js\/types<\/code> directory for all shared types:<\/p>\n<pre><code>\/\/ resources\/js\/types\/models.ts\r\n\r\nexport interface User {\r\n    id: number;\r\n    name: string;\r\n    email: string;\r\n    created_at: string;\r\n    updated_at: string;\r\n}\r\n\r\nexport interface Project {\r\n    id: number;\r\n    name: string;\r\n    description: string | null;\r\n    status: 'active' | 'paused' | 'completed' | 'cancelled';\r\n    user_id: number;\r\n    created_at: string;\r\n    updated_at: string;\r\n\r\n    \/\/ Relationships (optional \u2014 only present when eager loaded)\r\n    tasks?: Task[];\r\n    owner?: User;\r\n}\r\n\r\nexport interface Task {\r\n    id: number;\r\n    project_id: number;\r\n    title: string;\r\n    description: string | null;\r\n    status: 'pending' | 'in_progress' | 'completed';\r\n    priority: 'low' | 'medium' | 'high' | 'urgent';\r\n    due_date: string | null;\r\n    completed_at: string | null;\r\n    created_at: string;\r\n    updated_at: string;\r\n}\r\n\r\n\/\/ Pagination wrapper\r\nexport interface Paginated&lt;T&gt; {\r\n    data: T[];\r\n    current_page: number;\r\n    last_page: number;\r\n    per_page: number;\r\n    total: number;\r\n    from: number | null;\r\n    to: number | null;\r\n    links: Array&lt;{\r\n        url: string | null;\r\n        label: string;\r\n        active: boolean;\r\n    }&gt;;\r\n}<\/code><\/pre>\n<pre><code>\/\/ resources\/js\/types\/inertia.d.ts\r\n\/\/ Type Inertia page props globally\r\nimport { User } from '.\/models';\r\n\r\ndeclare module '@inertiajs\/react' {\r\n    interface PageProps {\r\n        auth: {\r\n            user: User;\r\n        };\r\n        flash: {\r\n            success?: string;\r\n            error?: string;\r\n        };\r\n    }\r\n}<\/code><\/pre>\n<h2>Step 4 \u2014 Type Inertia Page Components<\/h2>\n<pre><code>\/\/ resources\/js\/Pages\/Projects\/Index.tsx\r\nimport { Head, Link } from '@inertiajs\/react';\r\nimport type { PageProps } from '@inertiajs\/react';\r\nimport type { Project, Paginated } from '@\/types\/models';\r\nimport AppLayout from '@\/Layouts\/AppLayout';\r\n\r\n\/\/ Typed page props\r\ninterface Props extends PageProps {\r\n    projects: Paginated&lt;Project&gt;;\r\n    filters: {\r\n        search?: string;\r\n        status?: string;\r\n    };\r\n}\r\n\r\nexport default function ProjectsIndex({ projects, filters }: Props) {\r\n    return (\r\n        &lt;AppLayout&gt;\r\n            &lt;Head title=\"Projects\" \/&gt;\r\n\r\n            &lt;div&gt;\r\n                {projects.data.map((project) =&gt; (\r\n                    &lt;Link key={project.id} href={`\/projects\/${project.id}`}&gt;\r\n                        &lt;div&gt;\r\n                            &lt;h3&gt;{project.name}&lt;\/h3&gt;\r\n                            &lt;span&gt;{project.status}&lt;\/span&gt;\r\n                            {\/* TypeScript knows project.tasks is Task[] | undefined *\/}\r\n                            {project.tasks &amp;&amp; (\r\n                                &lt;span&gt;{project.tasks.length} tasks&lt;\/span&gt;\r\n                            )}\r\n                        &lt;\/div&gt;\r\n                    &lt;\/Link&gt;\r\n                ))}\r\n            &lt;\/div&gt;\r\n\r\n            &lt;div&gt;\r\n                Page {projects.current_page} of {projects.last_page}\r\n            &lt;\/div&gt;\r\n        &lt;\/AppLayout&gt;\r\n    );\r\n}<\/code><\/pre>\n<h2>Step 5 \u2014 Typed Forms with Inertia<\/h2>\n<pre><code>\/\/ resources\/js\/Pages\/Projects\/Create.tsx\r\nimport { useForm } from '@inertiajs\/react';\r\n\r\ninterface CreateProjectForm {\r\n    name: string;\r\n    description: string;\r\n    status: 'active' | 'paused';\r\n}\r\n\r\nexport default function CreateProject() {\r\n    const { data, setData, post, processing, errors } = useForm&lt;CreateProjectForm&gt;({\r\n        name:        '',\r\n        description: '',\r\n        status:      'active',\r\n    });\r\n\r\n    \/\/ TypeScript knows data.name is string\r\n    \/\/ TypeScript knows data.status is 'active' | 'paused' \u2014 not any string\r\n    \/\/ TypeScript errors if you try to set data.unknown_field\r\n\r\n    return (\r\n        &lt;form onSubmit={(e) =&gt; { e.preventDefault(); post('\/projects'); }}&gt;\r\n            &lt;input\r\n                value={data.name}\r\n                onChange={(e) =&gt; setData('name', e.target.value)}\r\n            \/&gt;\r\n            {errors.name &amp;&amp; &lt;p&gt;{errors.name}&lt;\/p&gt;}\r\n\r\n            &lt;select\r\n                value={data.status}\r\n                onChange={(e) =&gt; setData('status', e.target.value as CreateProjectForm['status'])}\r\n            &gt;\r\n                &lt;option value=\"active\"&gt;Active&lt;\/option&gt;\r\n                &lt;option value=\"paused\"&gt;Paused&lt;\/option&gt;\r\n            &lt;\/select&gt;\r\n\r\n            &lt;button type=\"submit\" disabled={processing}&gt;Create&lt;\/button&gt;\r\n        &lt;\/form&gt;\r\n    );\r\n}<\/code><\/pre>\n<h2>Step 6 \u2014 Typed Routes with Ziggy<\/h2>\n<p>Ziggy generates JavaScript route helpers from your Laravel routes \u2014 and with TypeScript, you get full type safety on route parameters:<\/p>\n<pre><code>composer require tightenco\/ziggy<\/code><\/pre>\n<pre><code>npm install --save-dev ziggy-js<\/code><\/pre>\n<pre><code>\/\/ routes\/web.php \u2014 add names to all routes\r\nRoute::get('\/projects', [ProjectController::class, 'index'])-&gt;name('projects.index');\r\nRoute::get('\/projects\/{project}', [ProjectController::class, 'show'])-&gt;name('projects.show');\r\nRoute::post('\/projects', [ProjectController::class, 'store'])-&gt;name('projects.store');<\/code><\/pre>\n<pre><code>\/\/ Generate TypeScript types from your routes\r\nphp artisan ziggy:generate --types<\/code><\/pre>\n<pre><code>\/\/ resources\/js\/Pages\/ProjectShow.tsx\r\nimport { Link } from '@inertiajs\/react';\r\nimport { route } from 'ziggy-js';\r\n\r\nexport default function ProjectShow({ project }) {\r\n    return (\r\n        &lt;div&gt;\r\n            {\/* TypeScript knows projects.show requires {project: number} *\/}\r\n            &lt;Link href={route('projects.show', { project: project.id })}&gt;\r\n                View Project\r\n            &lt;\/Link&gt;\r\n\r\n            {\/* TypeScript ERROR: missing required parameter *\/}\r\n            {\/* &lt;Link href={route('projects.show')}&gt; *\/}\r\n        &lt;\/div&gt;\r\n    );\r\n}<\/code><\/pre>\n<h2>Step 7 \u2014 Laravel Data for Shared Types (Advanced)<\/h2>\n<p>The <code>spatie\/laravel-data<\/code> package can automatically generate TypeScript types from your PHP Data classes \u2014 eliminating manual type duplication:<\/p>\n<pre><code>composer require spatie\/laravel-data\r\nnpm install --save-dev @spatie\/laravel-typescript-transformer<\/code><\/pre>\n<pre><code>&lt;?php\r\n\r\n\/\/ app\/Data\/ProjectData.php\r\nuse Spatie\\LaravelData\\Data;\r\nuse Spatie\\TypeScriptTransformer\\Attributes\\TypeScript;\r\n\r\n#[TypeScript]\r\nclass ProjectData extends Data\r\n{\r\n    public function __construct(\r\n        public readonly int $id,\r\n        public readonly string $name,\r\n        public readonly ?string $description,\r\n        public readonly ProjectStatus $status, \/\/ enum\r\n        public readonly Carbon $created_at,\r\n    ) {}\r\n\r\n    public static function fromModel(Project $project): self\r\n    {\r\n        return new self(\r\n            id:          $project-&gt;id,\r\n            name:        $project-&gt;name,\r\n            description: $project-&gt;description,\r\n            status:      $project-&gt;status,\r\n            created_at:  $project-&gt;created_at,\r\n        );\r\n    }\r\n}<\/code><\/pre>\n<pre><code># Generate TypeScript types from PHP Data classes\r\nphp artisan typescript:transform<\/code><\/pre>\n<p>This generates:<\/p>\n<pre><code>\/\/ auto-generated \u2014 do not edit\r\nexport type ProjectData = {\r\n    id: number;\r\n    name: string;\r\n    description: string | null;\r\n    status: 'active' | 'paused' | 'completed' | 'cancelled';\r\n    created_at: string;\r\n}<\/code><\/pre>\n<p>Your PHP and TypeScript types stay in sync automatically \u2014 change the PHP class and regenerate.<\/p>\n<h2>TypeScript Strict Mode \u2014 Common Errors and Fixes<\/h2>\n<pre><code>\/\/ Error: Object is possibly null\r\nconst name = user?.name; \/\/ \u2705 optional chaining\r\nif (user) { const name = user.name; } \/\/ \u2705 null check\r\n\r\n\/\/ Error: Element implicitly has 'any' type\r\nconst value = data['key']; \/\/ \u274c\r\nconst value = (data as Record&lt;string, unknown&gt;)['key']; \/\/ \u2705\r\n\r\n\/\/ Error: Type 'string' is not assignable to status enum\r\nsetData('status', 'unknown'); \/\/ \u274c TypeScript catches this\r\nsetData('status', 'active');  \/\/ \u2705\r\n\r\n\/\/ Error: Property does not exist on type\r\nproject.unknown_field; \/\/ \u274c TypeScript catches this\r\nproject.name; \/\/ \u2705<\/code><\/pre>\n<p>If you&#8217;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, <a href=\"https:\/\/softcrony.com\/contact\/\">our frontend team at Softcrony can help<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 \u2014 from Vite configuration to shared types between your PHP backend and React [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":109,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[51,50,19,48,106,18],"class_list":["post-107","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","tag-frontend","tag-javascript","tag-laravel","tag-react","tag-typescript","tag-web-development"],"_links":{"self":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/107","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/comments?post=107"}],"version-history":[{"count":2,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/107\/revisions"}],"predecessor-version":[{"id":110,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/107\/revisions\/110"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/media\/109"}],"wp:attachment":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/media?parent=107"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/categories?post=107"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/tags?post=107"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}