Vue 3 with TypeScript: Common Pitfalls to Avoid

Vue 3 has solid TypeScript support, but a few patterns lose type safety quietly. Here's what to watch for in props, refs, and emits.

Richard GamoraRichard GamoraFullstack developer·4 min read
VueTypeScript

Vue 3 has the best TypeScript story Vue has ever had — but a handful of patterns lose type safety in ways that look fine until you read the inferred types. Here are the ones I see most.

Always type props with defineProps

Use the generic form: defineProps<{ id: string; active?: boolean }>(). Avoid the runtime form ({ id: String, active: Boolean }) — it works but TypeScript only sees them as the broad String, Boolean types, not literal types.

If you need defaults, withDefaults(defineProps<...>(), { active: false }) preserves the typed shape and applies defaults at runtime.

Type ref values when they start undefined

ref(null) infers Ref<null>, which is rarely what you want. Use ref<User | null>(null) so TypeScript knows what the ref will hold once data arrives. The same applies to template refs: const inputEl = ref<HTMLInputElement | null>(null).

Type emits as a tuple

tsconst emit = defineEmits<{
  (e: 'update:modelValue', value: string): void
  (e: 'submit'): void
}>()

This catches typos in event names and ensures the payload matches. Without typed emits, emit('typo', value) compiles fine and silently does nothing.

Composable return types

When a composable returns reactive state, return refs (or a reactive object), but return them in a stable shape across all branches. If one path returns { user, loading } and another returns { user, error, loading }, the inferred return type becomes a union and consumers have to narrow it. Pick one shape and always return it.

About the author

Richard Gamora

Richard Gamora

Fullstack developer based in the Philippines, working mostly with Laravel and Vue.js, with eight years of production experience across web and mobile.

me@richardgamora.comUpwork ↗

More on Vue & Nuxt