Vue

7 / 10

Vue

TypeScript with Vue: A Pragmatic Setup

Vue 3 was written in TypeScript and it shows. Typing props, emits, refs, and composables without drowning in generics.

TypeScript in Vue used to mean fighting the framework; Vue 3 ended that by being written in TypeScript from the ground up. With <script setup lang="ts"> and the Volar-based editor tooling, types now flow from your props through your templates, and the payoff is autocomplete and refactoring confidence across every component boundary.

The core patterns, quickly

  • Props: defineProps<{ user: User; compact?: boolean }>(), with withDefaults when defaults matter. Interfaces beat runtime validators for expressiveness.
  • Emits: defineEmits<{ save: [draft: Draft]; cancel: [] }>() gives every listener a typed payload.
  • Refs: inference handles most cases; annotate the nullable ones explicitly, like ref<User | null>(null), and type template refs with the element or exposed component type.
  • v-model: defineModel<string>() collapses the old props-plus-emit dance into one typed line.

Type the seams, infer the middle

The highest-value types sit at boundaries: API responses, composable signatures, store state, and component props. Inside a function, inference does the work, and annotating every local variable is ceremony without safety. Define API types once, ideally generated from your backend schema or validated at runtime with something like zod at the fetch boundary, so a field rename breaks the build instead of production.

Keep strictness honest

Turn on strict mode from day one on new projects; retrofitting it later is the expensive path. Treat any as a loan with interest, acceptable at a messy third-party boundary, corrosive in your own domain logic. In Nuxt, let the generated types work for you: typed useFetch returns, typed route params, and typed auto-imports come free once you stop fighting the defaults. The goal is not maximal typing; it is that the compiler catches the mistakes you were going to make anyway.