Anjjar logo
  • Homepage
  • /
  • Blog
  • /
  • TypeScript in Vue 3: The 4 Places That Pay Back Immediately

TypeScript in Vue 3: The 4 Places That Pay Back Immediately

Published on , updated on

TypeScript in Vue projects has a reputation for slowing you down. More code to write, more errors to fix, more tooling to configure. For developers new to the combination, the overhead can feel like it outweighs the benefit - at least for the first few days.

It does not. But the payoff is not evenly distributed. Some TypeScript adds overhead with minimal return. Some TypeScript prevents hours of debugging. This guide focuses on the four specific places in a Vue 3 project where TypeScript delivers the highest return on investment - and explains, for each one, exactly why.

Why TypeScript Matters More in Vue 3 Than in Vue 2

Vue 3 was rewritten in TypeScript from the ground up. The Composition API, defineProps generics, and Pinia were all designed with TypeScript as a first-class concern - not as an afterthought. The type inference in Vue 3 is deep enough that many types are inferred automatically without annotation. You benefit from TypeScript even when you write less of it.

Vue 2's TypeScript support relied on decorators and class components - an approach that felt bolted-on. Vue 3's Composition API is fundamentally function-based, which maps cleanly to TypeScript's type system. The combination is now genuinely ergonomic.

Place 1: API Response Types

The highest-value TypeScript investment in any Vue project: defining interfaces for your API responses. This is the place where TypeScript catches bugs before they reach production most reliably.

Without types, an API response is typed as any. When the backend team renames a field - user.name becomes user.fullName - the JavaScript code compiles perfectly. The bug surfaces at runtime, in production, when a user sees a blank field or a crash. With TypeScript, the same change causes a compile error at every usage site, caught before the build deploys.

// Without TypeScript - backend renames field, nothing breaks at compile time
const { data: user } = await useFetch('/api/user')
const name = user.value.name // undefined in production after rename

// With TypeScript - rename caught immediately everywhere it is used
interface User {
  id: number
  fullName: string  // renamed from 'name'
  email: string
  role: 'admin' | 'user' | 'viewer'
}

const { data: user } = await useFetch<User>('/api/user')
// user.value.name  ← TypeScript error: Property 'name' does not exist

🔑 KEY:  API response types return their investment the first time a backend change is caught before deployment rather than in production. On any team with backend communication, this happens regularly.

Place 2: Component Props with defineProps<{}>

Vue 3 with TypeScript enables fully type-safe component props through the generic form of defineProps<{}>. This is one of the cleanest TypeScript integrations in any framework - zero boilerplate, full inference.

The parent gets a type error if it passes the wrong prop type. The template gets autocomplete on every prop. Optional props are marked with ?. Union types constrain the allowed values. All of this from a single type annotation.

// Vue 2 / early Vue 3 - PropType approach (verbose, limited type safety)
props: {
  userId: { type: Number as PropType<number>, required: true },
  status: { type: String as PropType<'active'|'pending'>, required: true },
}
  
// Vue 3 + TypeScript - defineProps generic (clean, fully type-checked)
const props = defineProps<{
  userId: number
  label: string
  isActive?: boolean              // optional
  status: 'active' | 'pending' | 'inactive'  // union
  onSelect?: (id: number) => void  // typed callback
}>()
  
// TypeScript infers correctly in template and script:
// props.userId is number
// props.status is the union type - no arbitrary strings allowed

💡 TIP: Need default values with defineProps generics? Use withDefaults() wrapper: const props = withDefaults(defineProps<{count?: number}>(), { count: 0 }). Clean, type-safe, no runtime PropType required.

Place 3: Composable Return Types

Every composable you write should have an explicitly typed return value. Without it, TypeScript infers the return type from the implementation - which means the return type silently changes when you modify internal logic. An explicit return type makes the public contract visible and enforced.

The return type interface also serves as documentation. Any developer reading the composable call site knows exactly what they receive - without reading the implementation.

// Define the contract explicitly
interface UseListingsReturn {
  listings:     Ref<Listing[]>
  isLoading:    Ref<boolean>
  error:        Ref<Error | null>
  fetchListings: (filters?: ListingFilters) => Promise<void>
  clearListings: () => void
}
  
export function useListings(): UseListingsReturn {
  const listings  = ref<Listing[]>([])
  const isLoading = ref(false)
  const error     = ref<Error | null>(null)
  
  async function fetchListings(filters?: ListingFilters) {
    isLoading.value = true
    try {
      listings.value = await $fetch('/api/listings', { query: filters })
    } catch(e) { error.value = e as Error }
    finally { isLoading.value = false }
  }
  
  function clearListings() { listings.value = [] }
  
  return { listings, isLoading, error, fetchListings, clearListings }
  // TypeScript verifies the return matches UseListingsReturn
}

Place 4: Pinia Store State and Actions

Pinia with TypeScript is one of the cleanest state management experiences in any framework. For simple stores, TypeScript infers everything automatically from the store definition - no manual type declarations needed.

For stores with complex state shapes, defining an explicit state interface ensures all actions maintain the contract, and gives every component that uses the store correct autocomplete and type checking.

interface ListingFilters {
  priceMax:  number
  bedrooms:  number
  location:  string | null
}
  
interface ListingsState {
  items:      Listing[]
  selectedId: number | null
  filters:    ListingFilters
  isLoading:  boolean
}
  
export const useListingsStore = defineStore('listings', () => {
  const state = reactive<ListingsState>({
    items:      [],
    selectedId: null,
    filters:    { priceMax: 5000, bedrooms: 1, location: null },
    isLoading:  false,
  })
  
  function selectListing(id: number) {
    state.selectedId = id         // ✅ TypeScript: id is number
    // state.selectedId = 'abc'   // ← compile error
  }
  
  return { ...toRefs(state), selectListing }
})

What to Avoid: TypeScript Overhead Without Return

Not all TypeScript adds value. These patterns add complexity without meaningful protection:

  • Annotating every local variable when TypeScript infers it correctly - const count = ref(0) is already Ref<number>
  • Writing complex generic types for simple, obvious cases
  • Using 'as any' or '@ts-ignore' to silence errors - fix the type, do not suppress the warning
  • Creating type aliases for primitives - type UserId = number adds no value
  • Over-specifying return types on very short functions where inference is obvious

💡 TIP:  TypeScript should make code clearer, not harder to read. When a type annotation requires more explanation than the function itself, that is a signal to simplify the underlying design.

Setting Up TypeScript in a Nuxt 3 Project

Nuxt 3 ships with TypeScript support out of the box. The tsconfig.json is generated automatically. For strict mode - which catches the most errors - add the following to your nuxt.config.ts:

// nuxt.config.ts
export default defineNuxtConfig({
  typescript: {
    strict: true,           // enables all strict checks
    typeCheck: true,        // runs tsc alongside the dev server
    shim: false,            // not needed with Vite
  }
})
  
// Run type check standalone:
// npx nuxi typecheck

⚠️ WARNING: typeCheck: true runs TypeScript on every file save during development. On large projects this can slow down the dev server. Consider running npx nuxi typecheck as a pre-commit or CI step instead.

Conclusion

The four areas where TypeScript delivers immediate value in Vue 3 - API response types, defineProps generics, composable return types, and Pinia stores - represent the interfaces in your application where mismatches are most expensive. Catching a type error at compile time rather than in production is the difference between a five-minute fix and a production incident.

Start with API response types. Define interfaces for your three most-used API endpoints. Run the type checker. The first time it catches a backend change before it deploys, TypeScript will have paid for itself in this project.

Brahim anjjar frontend developer (Nuxtjs/Vuejs)
Brahim Anjjar

Brahim Anjjar is a frontend engineer with 5+ years of production experience in Vue.js 3, Nuxt.js 3, and TypeScript. He is a Certified Nuxt Master who has worked with clients in Morocco, France, and Martinique - building platforms where technical SEO is an architecture constraint from day one, not a post-launch afterthought.

Contactez-moi

Vous avez un projet web, une question ou besoin d’un devis ? N'hésitez pas à me contacter, je serai ravie d'échanger avec vous.

Téléphone :  +212 620-350962
Sitemap
Anjjar logo

Développeur Front-End se concentrant sur la création d'applications web performantes en termes de vitesse et de SEO en utilisant Nuxt.js et Vue.js

© 2024 Anjjar, All rights reserved