Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

TypeScript with Vue 3 Best Practices

By Kokil Thapa | Last reviewed: September 2026

TypeScript with Vue 3 best practices turn a fast-moving front end into something your team can refactor without fear. Vue 3's Composition API, Pinia, and Vite 8.x make typed components practical on real projects. Yet many teams still treat types as optional decoration. That choice shows up later as broken builds, silent prop mismatches, and API contract bugs. This guide covers the setup, typing patterns, and CI checks I rely on when shipping Vue inside Laravel and Vue full-stack applications.

How do you set up TypeScript with Vue 3 for a new project in 2026?

Start from the official scaffold rather than bolting TypeScript onto a plain JavaScript project later. The Vue CLI era is largely behind us. Vite 8.x with the Vue plugin is the standard path today.

Run the create command with the TypeScript template on Node.js 26 LTS:

npm create vue@latest my-app
cd my-app
npm install
npm run dev

During prompts, enable TypeScript, Vue Router, and Pinia if your app needs routing or shared state. The generated tsconfig.app.json, tsconfig.node.json, and env.d.ts files give you a sane baseline. Read TypeScript config explained for beginners if split configs feel opaque at first.

Enable strict mode immediately

Do not defer strictness. Flip these flags in tsconfig.app.json on day one:

{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "verbatimModuleSyntax": true
  }
}

Strict mode catches nullable API fields, forgotten await calls, and loose object shapes before they reach QA. On client projects I maintain, teams that postpone strict mode rarely turn it on later. The migration cost only grows.

Install the right editor tooling

Use the Vue Official extension (Volar). Disable Vetur if it is still installed. Vetur does not understand Vue 3 script setup and will show false errors on valid code.

Add a type-check script that runs outside the dev server:

npm install -D vue-tsc typescript
npm pkg set scripts.type-check="vue-tsc --build --force"

The dev server transpiles quickly. It does not guarantee type safety. Treat vue-tsc as the gate that blocks merges.

Vue 3 + TypeScript ToolchainVolar IDEVue Official extVite 8.xFast HMR devvue-tscType-check gateDeployProductionProject Files That Mattertsconfig.app.jsonenv.d.tscomponents/*.vuestores/*.tsrouter/index.tsvite.config.ts
TypeScript with Vue 3 best practices start with Volar, strict tsconfig, and vue-tsc before production deploy.

What are the best typing patterns for Vue 3 Composition API components?

The Composition API is where TypeScript pays off most. Options API typing works, but script setup with typed macros is cleaner and easier to maintain. See the Vue 3 Composition API deep dive for runtime behaviour; this section focuses on types.

Type props with defineProps

Prefer the type-based macro form. It reads well and composes with interfaces:

<script setup lang="ts">
interface BookingFormProps {
  tourId: number
  currency?: 'NPR' | 'USD'
  readonly?: boolean
}

const props = withDefaults(defineProps<BookingFormProps>(), {
  currency: 'NPR',
  readonly: false,
})
</script>

Extract shared prop interfaces into src/types/components.ts when multiple components reuse the same shape. Do not duplicate inline object types across ten files.

Type emits with defineEmits

Typed emits document the contract between child and parent. They also catch typos in event names at compile time:

<script setup lang="ts">
const emit = defineEmits<{
  submit: [payload: { tourId: number; guests: number }]
  cancel: []
}>()

function onSubmit() {
  emit('submit', { tourId: props.tourId, guests: 2 })
}
</script>

Type template refs and composables

Template refs need explicit element or component types. Without them, ref.value becomes any and strict mode loses meaning:

import { ref, onMounted } from 'vue'

const inputEl = ref<HTMLInputElement | null>(null)

onMounted(() => {
  inputEl.value?.focus()
})

Composable functions should declare return types when inference is ambiguous. Export reusable composables from src/composables/ with explicit interfaces for complex return objects. That pattern scales well on booking dashboards and admin panels I have shipped with Livewire and Vue hybrids.

How should you type props, emits, and shared state in Vue 3?

Props and emits define component boundaries. Pinia stores define application boundaries. Treat all three as published APIs. Consumers should not guess field names or payload shapes.

Pinia with TypeScript

Pinia is the default store for Vue 3. The setup-store style maps cleanly to TypeScript. See Pinia vs Vuex 4 for Vue 3 state management for migration context:

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Tour } from '@/types/tour'

export const useTourStore = defineStore('tour', () => {
  const tours = ref<Tour[]>([])
  const loading = ref(false)

  const activeCount = computed(() =>
    tours.value.filter(t => t.status === 'active').length
  )

  async function fetchTours(): Promise<void> {
    loading.value = true
    try {
      const res = await fetch('/api/tours')
      tours.value = await res.json() as Tour[]
    } finally {
      loading.value = false
    }
  }

  return { tours, loading, activeCount, fetchTours }
})

Define API response types in src/types/ and import them into stores and components. When the Laravel backend changes a field, TypeScript surfaces every front-end reference. That feedback loop saves hours during Laravel API upgrades.

Vue Router 4 typed routes

Route params and meta fields benefit from a central route record type. Untyped params invite NaN bugs when you call Number(route.params.id) on undefined input:

import type { RouteRecordRaw } from 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    title?: string
  }
}

const routes: RouteRecordRaw[] = [
  {
    path: '/tours/:id',
    name: 'tour-show',
    component: () => import('@/views/TourShow.vue'),
    meta: { requiresAuth: true, title: 'Tour Details' },
  },
]

For advanced navigation guards and lazy routes, read Vue Router 4 advanced patterns.

Component Type BoundariesParent ViewPasses typed propsListens to emitsChild ComponentdefinePropsdefineEmitsPinia StoreShared typed stateActions + gettersShared Types Layer (src/types/)Tour.ts Booking.ts ApiResponse.ts User.tsSingle source of truth for API and UI contracts
Typed props, emits, and Pinia stores share interfaces from a central types folder in Vue 3 TypeScript apps.

Which TypeScript tooling catches Vue 3 errors before production deploy?

Local dev comfort is not production safety. You need automated checks in CI that fail on type errors, unused suppressions, and broken imports. I wire the same pattern into build pipeline automation for sister sites on shared GitLab runners.

The minimum CI script set

  1. npm run type-check — runs vue-tsc --build across the project.
  2. npm run build — confirms Vite production output succeeds.
  3. npm run test:unit — optional but valuable for composable logic.

A typical GitLab CI job block:

typecheck:
  image: node:26
  script:
    - npm ci
    - npm run type-check
    - npm run build

Run type-check before build. A green Vite build can still ship broken types if you skip vue-tsc.

Options API vs script setup: a practical comparison

Both work with TypeScript. For greenfield Vue 3 code, script setup wins on brevity and inference. Options API remains valid for legacy files you migrate incrementally.

CriteriaScript setup + TSOptions API + TS
Type inferenceStrong with defineProps macrosRequires propType generics or external wrappers
BoilerplateLow — no return objectHigher — data, methods, computed blocks
Composable reuseNative — import and callMixin-based patterns age poorly
Migration costDefault for new filesKeep for stable legacy components
Editor supportBest with VolarGood but noisier on large components

My rule on production apps: new features use script setup. Touch a legacy Options component only when the business change requires it. Then add types while you are there.

Validate API payloads at the boundary

TypeScript erases at runtime. A typed interface does not prove the JSON from your server matches. Parse responses with a runtime validator at the network boundary. Zod is a common choice. Keep validators next to API client modules:

import { z } from 'zod'

export const TourSchema = z.object({
  id: z.number(),
  title: z.string(),
  priceNpr: z.number(),
})

export type Tour = z.infer<typeof TourSchema>

When debugging malformed JSON during integration, paste samples into the JSON formatter tool to inspect structure before you update schemas.

CI Pipeline for Typed Vue AppsGit PushFeature branchnpm ciLockfile installvue-tscType-check gatevite buildProd assetsFail PRBlock mergeType errors stop the pipelineDeploy to Laravel public/buildSymlink release via Deployer 7
Run vue-tsc before vite build in CI so TypeScript with Vue 3 best practices block bad merges early.

How do you integrate a typed Vue 3 front end with a Laravel backend?

Most of my Vue work sits inside Laravel apps, not standalone SPAs. Adventure Third Pole Trek and similar booking platforms use Laravel for auth, payments, and PDF workflows. Vue handles interactive dashboards. The integration point is the API contract.

Share types without sharing repos blindly

You rarely want Laravel PHP types auto-generated on every front-end save unless you already run a monorepo toolchain. Practical options ranked by effort:

  • OpenAPI spec — Generate TypeScript clients from a documented REST API. Best for public or partner APIs.
  • Hand-maintained src/types/api.ts — Fine for small teams with stable endpoints.
  • JSON Schema from Form Requests — Useful when validation rules are the source of truth.

Align field naming in PHP resources and TypeScript interfaces. Mixed camelCase and snake_case causes silent mapping bugs. Pick one convention at the JSON boundary and transform in one place.

Axios or fetch wrappers with typed errors

import axios from 'axios'
import type { AxiosError } from 'axios'
import type { ValidationErrorResponse } from '@/types/api'

export const api = axios.create({
  baseURL: '/api',
  headers: { Accept: 'application/json' },
})

export function isValidationError(
  err: unknown
): err is AxiosError<ValidationErrorResponse> {
  return axios.isAxiosError(err) && err.response?.status === 422
}

Centralize interceptors for CSRF tokens and 401 redirects. Laravel Sanctum cookie auth needs consistent withCredentials settings. Document that in your project README so the next developer does not break session auth.

Build output and deployment

Commit built assets or build in CI depending on your server constraints. Several sites I deploy have no Node on production. We build on the runner with npm 12, commit public/build, and ship via Deployer symlink releases. After deploy, reload PHP-FPM so opcache picks up changed PHP files even when only the manifest changed.

For larger SPAs that need SSR, evaluate Vue 3 SSR with Nuxt 3 separately. Most Laravel dashboards do not need SSR on day one.

What common TypeScript with Vue 3 mistakes break production builds?

These failures recur across teams new to typed Vue. Each one is cheap to prevent and expensive to debug under deadline pressure.

Using any to silence errors

any defeats the purpose of strict mode. Replace it with unknown and narrow, or define a proper interface. If third-party libraries ship poor types, wrap them in a small typed adapter module. Do not spread any into your domain code.

Ignoring .vue module declarations

Ensure env.d.ts includes the Vue module shim:

/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

Without this file, imports like import App from './App.vue' fail type-check even when the app runs fine in dev.

Skipping generic constraints on composables

export function useList<T extends { id: number }>(items: Ref<T[]>) {
  const byId = (id: number) => items.value.find(i => i.id === id)
  return { byId }
}

Generics keep composables reusable without casting. They pay off when the same list helper serves tours, lawyers, and products on directory sites like Lawyers Pokhara.

Forgetting SEO and SPA routing

Typed Vue SPAs still need crawlable URLs and metadata. Technical SEO for interactive apps is a separate concern. Read SEO for single-page applications and wire meta tags through Vue Router guards or a head manager.

Vue 3 TS TroubleshootingBuild or type error?Dev works, CI failsRun vue-tsc locallyImport .vue failsCheck env.d.tsStill failing?Run vue-tsc -p tsconfig.app.jsonDisable VeturEnable Vue Official extFix root cause — never commit @ts-ignore
When TypeScript with Vue 3 best practices fail in CI, run vue-tsc locally and verify env.d.ts plus Volar before merging.

Consult the official Vue.js TypeScript guide for macro syntax updates. Cross-check compiler behaviour against the TypeScript handbook when strict flags behave unexpectedly. For Vite-specific module resolution, the Vite documentation covers vite/client types and path aliases.

If you are migrating from plain JavaScript, work file by file. Enable allowJs temporarily, type new modules first, and tighten legacy code when you touch it. That incremental path matches how I upgrade long-lived portals without a risky big-bang rewrite. Pair the effort with TypeScript for JavaScript developers for mental model shifts around structural typing.

Quality assurance belongs in the release process, not only in the editor. Add typed component tests for critical composables and run them in CI alongside vue-tsc. For broader QA strategy on production apps, see testing and optimization services and Laravel feature testing best practices for the PHP side of the stack.

On eCommerce and booking UIs, typed filters and cart state reduce checkout bugs. Product filter UX patterns from eCommerce product filters UX best practices apply directly to typed Pinia stores backing faceted search. The Adventure Third Pole Trek booking platform is a real example of Laravel plus interactive front-end modules working together.

When scoping a new typed Vue module for your business, custom software development and web development services cover everything from component libraries to full deploy pipelines. For greenfield SPAs with heavy interactivity, enterprise application development may fit better than ad-hoc page scripts.

Keep path aliases consistent between vite.config.ts and tsconfig.app.json. A common failure mode is @/components/Foo.vue resolving in Vite but failing in vue-tsc because only one config defines the alias. Mirror the resolve.alias block in both files.

Prefer explicit return types on public composables exported from packages shared across apps. Internal components can rely on inference. The distinction keeps library boundaries clear when multiple teams import the same code.

Document npm engine requirements in package.json so CI and local machines stay on Node.js 26 LTS. Type-check results can differ across major Node versions when native dependencies are involved.

Key Takeaways

  • Scaffold with the official Vue + TypeScript template, enable strict mode immediately, and use Volar instead of Vetur.
  • Type props, emits, refs, Pinia stores, and API responses as published contracts—not optional annotations.
  • Run vue-tsc --build in CI before vite build; a green dev server does not prove type safety.
  • Validate JSON at the network boundary with Zod or similar; TypeScript types disappear at runtime.
  • Align Laravel API field naming with front-end interfaces and centralize HTTP error handling.
  • Fix root causes when type-check fails—never paper over errors with any or @ts-ignore.

People Also Ask

Is TypeScript worth it for Vue 3 in 2026?

Yes, for any Vue 3 app expected to live beyond a single release cycle. Types catch prop drift, refactor breakage, and API mismatches early. The upfront cost is one to two days of strict setup. Ongoing savings show up every time you rename a field or upgrade a dependency.

Should I use script setup or Options API with TypeScript?

Use script setup for all new components. It offers the cleanest typing with defineProps and defineEmits macros. Keep Options API files as-is until a business change requires editing them. Then add types during that edit rather than scheduling a standalone migration.

What is the difference between vue-tsc and tsc?

Standard tsc does not understand .vue single-file components. vue-tsc wraps the TypeScript compiler with Vue-aware processing so template and script blocks type-check together. Always use vue-tsc for Vue projects.

Can I use TypeScript Vue 3 with Laravel without Inertia?

Yes. Many Laravel apps mount Vue on specific Blade pages or serve a small SPA under /app. Sanctum handles auth. Vite compiles assets into public/build. You do not need Inertia unless you want server-driven routing with Vue page components.

Ship typed Vue 3 code you can maintain next year

TypeScript with Vue 3 best practices are not about chasing clever types. They are about making refactors safe, onboarding faster, and production bugs rarer. Start strict, type your boundaries, run vue-tsc in CI, and treat API contracts as shared truth between Laravel and Vue. That stack has survived real client deadlines on booking portals, legal-tech dashboards, and eCommerce admin panels. Need help wiring typed Vue modules into an existing Laravel app or greenfield build? Contact us to discuss architecture, CI setup, and deployment on your timeline.

Frequently Asked Questions

Strict tsconfig, typed defineProps and defineEmits, inferred Pinia state, vue-tsc in CI, and Volar instead of Vetur so .vue files type-check before deploy.

Start from the official scaffold, not a JavaScript project retrofitted later. Run npm create vue@latest on Node.js 26 LTS, enable TypeScript during prompts, and use the generated tsconfig.app.json, tsconfig.node.json, and env.d.ts baseline. Install dependencies, run the dev server, then add vue-tsc as a separate type-check script. Enable strict compiler flags on day one. Install the Vue Official extension and disable Vetur. On client projects I maintain, teams that skip this upfront setup pay much more when they try to tighten types months later.

Flip strict mode immediately in tsconfig.app.json: strict, noUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch, and verbatimModuleSyntax. Strict mode catches nullable API fields, forgotten await calls, and loose object shapes before QA. I have seen teams postpone strictness hoping to migrate gradually, but the migration cost only grows as untyped files accumulate. Treat strict tsconfig as non-negotiable for greenfield Vue 3 work. Pair it with vue-tsc in CI so compiler settings actually block bad merges instead of living only as editor warnings.

Use the Vue Official extension (Volar). Disable Vetur—it does not understand Vue 3 script setup and shows false errors on valid typed code.

Prefer the type-based defineProps macro with an interface, then apply defaults through withDefaults. Extract shared prop interfaces into src/types/components.ts when multiple components reuse the same shape instead of duplicating inline object types across ten files. Type-based props read cleanly, compose with interfaces, and work well with strict inference. On booking dashboards and admin panels I have shipped, central prop types catch breaking changes when a parent passes the wrong field type long before runtime testing surfaces the bug.

Use typed defineEmits with an object mapping event names to payload tuple types. That documents the contract between child and parent and catches typos in event names at compile time. For example, a submit event might carry tourId and guest count while cancel emits no payload. Typed emits pair naturally with typed props: both define component boundaries as published APIs. Consumers should not guess payload shapes. When refactoring event names across a large codebase, the compiler lists every broken reference instead of leaving silent runtime failures.

Use the setup-store style with defineStore, typing refs, computed values, and async actions explicitly. Import shared domain types from src/types/ rather than inlining shapes inside each store. When a Laravel backend changes a field, TypeScript surfaces every front-end reference in stores and components. That feedback loop saves hours during API upgrades on production apps I maintain. Return only what consumers need, keep loading flags typed as boolean, and type fetch responses against your API interfaces. Pinia with TypeScript treats application state as a published API, same as props and emits.

Define routes as RouteRecordRaw arrays and augment vue-router RouteMeta for requiresAuth, title, and other guard metadata. Untyped params invite NaN bugs when you call Number(route.params.id) on undefined input. Central route record typing keeps lazy-loaded views, meta fields, and navigation guards consistent. For advanced patterns like typed guards and code-split views, treat route definitions as part of your type surface alongside components and stores. On directory and booking apps, typed params prevent subtle ID parsing failures that only appear when users land on malformed deep links.

vue-tsc type-checks .vue single-file components outside the Vite dev server, which transpiles quickly but does not guarantee type safety. Install it as a dev dependency and add a type-check script using vue-tsc --build --force. Wire npm run type-check into GitLab CI or your pipeline before npm run build. I use the same pattern on sister sites with shared runners: npm ci, type-check, then build on Node.js 26. Treat vue-tsc as the gate that blocks merges. A green Vite production build can still ship broken types if you skip this step.

Both work, but script setup wins for greenfield code on brevity, inference, and composable reuse. Options API remains valid for stable legacy files you migrate incrementally. Script setup gives strong inference with defineProps macros, lower boilerplate, and native composable imports. Options API needs propType generics or external wrappers and ages poorly with mixin patterns. My rule on production apps: new features use script setup; touch legacy Options components only when the business change requires it, then add types while you are there. Volar support is best with script setup on large typed components.

TypeScript erases at runtime—a typed interface does not prove JSON from your server matches. Parse responses with a runtime validator like Zod at the network boundary, colocated with API client modules. Define a schema, infer the Tour or domain type from it, and reuse that type in Pinia stores and components. When debugging malformed JSON during Laravel integration, inspect samples before updating schemas. This pattern matters on booking platforms where a renamed PHP resource field would otherwise pass type-check but break checkout logic at runtime.

Treat the REST API contract as the integration point. Share types through OpenAPI-generated clients for public APIs, hand-maintained src/types/api.ts for small stable teams, or JSON Schema derived from Laravel Form Requests when validation rules are the source of truth. Pick one camelCase or snake_case convention at the JSON boundary and transform in one place. Wrap axios or fetch with typed validation errors, centralize CSRF and 401 interceptors, and document Sanctum cookie auth with consistent withCredentials settings. Build assets in CI with npm 12 when production servers have no Node, then deploy via symlink releases.

Using any to silence errors defeats strict mode—replace with unknown and narrow, or wrap poorly typed third-party code in small adapters. Missing env.d.ts Vue module shims breaks imports like App.vue during type-check even when dev runs fine. Skipping generic constraints on composables forces unsafe casts in reusable list helpers. Ignoring path alias alignment between vite.config.ts and tsconfig.app.json causes vue-tsc failures while Vite resolves correctly. Forgetting SEO on typed SPAs leaves crawlability gaps. When CI fails, run vue-tsc locally and verify env.d.ts plus Volar before merging.

Run type-check before build in CI. A green Vite build can still ship broken types if you skip vue-tsc.

Template refs need explicit element or component types—without them ref.value becomes any and strict mode loses meaning. Use HTMLInputElement or null unions for DOM refs and call optional chaining after mount. Composable functions should declare return types when inference is ambiguous. Export reusable composables from src/composables/ with explicit interfaces for complex return objects. Add generic constraints like T extends { id: number } on shared list helpers so the same composable serves tours, lawyers, and products without casting. That pattern scales well on admin panels and directory sites with repeated CRUD list behaviour.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: