Spring Sale 🍃 is live! Get 20% off ANY package with the coupon SPRINGBREAK

TypeScript with Laravel and Vue.js

With pure Vue.js installation, you have an interactive installer that helps you automatically plug in the TypeScript support. With a frontend directly integrated into Laravel, you need to install it manually.

Adding TypeScript support to your Laravel project is simple and only takes minutes. Let’s review the process with a fresh Laravel Breeze installation with Vue 3.

TypeScript installation

First, install the TypeScript compiler into your project.

npm add -D typescript
# or
yarn add -D typescript

TypeScript configuration

The next step is to configure the compiler. TypeScript requires a configuration file tsconfig.json in the root directory of your project.

The presence of a tsconfig.json file in a directory indicates that the directory is the root of a TypeScript project. The tsconfig.json file specifies the root files and the compiler options required to compile the project.

For many developers coming from Laravel, setting up the tsconfig.json file turns out to be a massive blocker due to the overflow of available options.

In reality, you should only care about a few essential properties, such as paths to your files and the level of strictness. The rest is pre-configured well and only requires attention in some edge cases.

Vue 3 provides the official TypeScript configuration, which contains the basic options required for Vue 3 with Vite.

Install the package @vue/tsconfig.

npm add -D @vue/tsconfig
# or
yarn add -D @vue/tsconfig

TL; DR. You can use a pre-configured Laravel-ready tsconfig.json, which enables pure JavaScript support and disables strict type-checking.

Create a file tsconfig.json and make it reference the installed configuration file.

{
"extends": "@vue/tsconfig/tsconfig.web.json",
}

Next, list the paths to the files that will contain TypeScript code in the include section of the config.

{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": [
"resources/**/*.ts",
"resources/**/*.d.ts",
"resources/**/*.vue"
]
}

If you’re just starting with TypeScript, it makes sense to start using types in new files and gradually add types to the existing components later. Your codebase does not have to reach 100% type coverage ever.

You can make TypeScript process pure .js files and disable strict type checking.

{
"compilerOptions": {
"allowJs": true,
"strict": false,
}
}

You may also want to create an alias referencing the root directory of your frontend in the paths section so that you can access the root directory using the @/ shorthand.

{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["resources/js/*"]
}
}
}

In order to get the support of Vite-specific methods and accessors, such as import.meta.glob, you need to register vite/client types in compilerOptions.types of your tsconfig.

{
"compilerOptions": {
"types": ["vite/client"],
}
}

From this point, you can already start writing typed code in .ts files. The boilerplate configuration file already supports .js files and pure JavaScript syntax, so you can keep the default entry point as is and start using TypeScript in new files and components only.


Switching to TypeScript

Initial Laravel installation comes with a boilerplate JavaScript entry point, which could be converted into TypeScript. All you need to do is rename .js to .ts and make slight changes.

First, change the main entry point file’s extension to .ts:

resources/js/app.js
resources/js/app.ts

Update the path in your app.blade.php:

@vite('resources/js/app.js')
@vite('resources/js/app.ts')

Instruct Vite to use the renamed entry point in vite.config.js.

import { defineConfig } from "vite"
import laravel from "laravel-vite-plugin"
export default defineConfig({
plugins: [
laravel({
input: "resources/js/app.js",
input: "resources/js/app.ts",
refresh: true,
}),
],
})

Now, you will need to make a few changes to the app.ts file.

This is the content of the basic app.js file shipped with Laravel Breeze:

import './bootstrap';
import '../css/app.css';
 
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
 
const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';
 
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob('./Pages/**/*.vue')
),
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue, Ziggy)
.mount(el);
},
progress: {
color: '#4B5563',
},
});

You may notice that your IDE throws a few errors. The issue is that TypeScript does allow you to use weak types (or no types at all) but doesn’t let you use incorrect types.

The first error you may encounter is the wrong return type of import.meta.glob. The thing is that glob is a generic method, and its return type could be defined explicitly. Import the type DefineComponent and apply it to the method.

import type { DefineComponent } from "vue";
 
createInertiaApp({
resolve: (name) => resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob("./Pages/**/*.vue")
import.meta.glob<DefineComponent>("./Pages/**/*.vue")
),
});

Another issue comes from Ziggy.js. First, Ziggy doesn’t provide TypeScript definitions.

The best solution. To get complete TypeScript support, it’s highly recommended to use the TypeScript-ready package Momentum Trail, which is built on top of Ziggy. The package supports all Ziggy’s methods and comes with IDE autocompletion and type-checking for route names and parameters.

You can omit this error by either applying a comment @ts-expect-error on top of the import line or setting noImplicitAny to false in the compiler options.

// @ts-expect-error
import { ZiggyVue } from "../../vendor/tightenco/ziggy/dist/vue.m";

or

{
"compilerOptions": {
"noImplicitAny": false,
}
}

You can use comments @ts-expect-error or @ts-ignore anywhere you feel like you don’t want to deal with insufficient or incorrect types.

Second, TypeScript can’t resolve Ziggy, as it’s declared on the window object, which doesn’t have a typed property called Ziggy. Since it’s just a configuration object which you’re not going to refer to manually, you shouldn’t care much about its type.

As a temporary solution, you can instruct the compiler to consider window a plain any object and access Ziggy from it.

createInertiaApp({
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue, Ziggy)
.use(ZiggyVue, (window as any).Ziggy)
.mount(el);
},
});

The as keyword is a Type Assertion in TypeScript, which tells the compiler to consider the object as another type than the type the compiler infers the object to be.

Vue components

You also may want to instruct the compiler and your IDE to interpret your components’ code as TypeScript. Append the lang="ts" part to the component’s script part.

<script lang="ts" setup>
interface Payload
{
name: string
amount: number
}
 
const submit = (payload: Payload) => {
//
}
</script>
<template>
...
</template>

Starting now, Vite can parse and compile TypeScript code in your Vue components.

Linting code

Even if your IDE can help you with real-time syntax highlighting and autocompletion, there’s still room for mistakes and type errors. We use a CLI tool vue-tsc to check them during the building process.

vue-tsc is a Vue-specific wrapper around tsc — a utility for command line type checking and type declaration generation.

npm add -D vue-tsc
# or
yarn add -D vue-tsc

To run the check manually, type the following command in the command line:

npx vue-tsc --noEmit

The --noEmit flag instructs tsc to not build output files and only perform the checks.

To perform the check automatically every time you build a project, modify the build script in the package.json file:

{
"scripts": {
"build": "vite build",
"build": "npm run type-check && vite build",
"type-check": "vue-tsc --noEmit"
}
}

That is it; you are all set! You can keep writing code the way you used to and start utilizing TypeScript features step-by-step.

VS Code Support

This section is only applicable to VS Code users.

Even though VS Code already has a built-in TypeScript engine, Volar (Vue.js extension) brings another TS service instance patched with Vue-specific support. To make Volar understand your plain .ts files and get them Vue.js features support, you need to install the official extension TypeScript Vue Plugin.

While this basic setup will work, you will have two TypeScript engine instances running simultaneously, which may lead to performance issues in larger projects.

To improve performance and prevent possible conflicts between two services, Volar provides a feature called “Takeover Mode.”

To enable Takeover Mode, you need to disable VS Code’s built-in TS language service in your project’s workspace only by following these steps:

  1. In your project workspace, bring up the command palette with Ctrl + Shift + P (macOS: Cmd + Shift + P).
  2. Type built and select “Extensions: Show Built-in Extensions.”
  3. Type typescript in the extension search box (do not remove @builtin prefix).
  4. Click the little gear icon of “TypeScript and JavaScript Language Features” and select “Disable (Workspace).”
  5. Reload the workspace. Takeover mode will be enabled when you open a Vue or TS file.

Package ecosystem

You might love to check the Inertia-ready package ecosystem Momentum to get comprehensive TypeScript support for essential Laravel features.

  • Trail — Frontend package to use Laravel routes with Inertia
  • Lock — Frontend package to use Laravel permissions with Inertia
  • Paginator — Headless wrapper around Laravel Pagination
  • Modal — Build dynamic modal dialogs in Inertia apps
  • Layout — Persistent layouts for Vue 3 apps
  • Preflight — Realtime backend-driven validation for Inertia apps
  • Vite Plugin Watch — Vite plugin to run shell commands on file changes

Use Inertia.js like a boss

Learn advanced concepts and make apps with Laravel and Inertia.js a breeze to build and maintain.

© 2023 Boris Lepikhin. All rights reserved.