Vue · TypeScript

Vue 3 + TypeScript — A Field Guide

Vue 3 was built with TypeScript in mind, and script setup makes typed components genuinely pleasant. The payoff is biggest exactly where bugs hurt most — props and shared logic.

John Kihiu12 min read

Vue 3 was designed with TypeScript as a first-class concern, and with <script setup> the experience is genuinely good rather than the awkward bolt-on it was in Vue 2. Types are not free — they add some ceremony — so the useful question is where they pay off most, and in Vue that is at component boundaries and in shared logic, exactly where a type mismatch otherwise turns into a runtime surprise.

Typed props and emits

The highest-value typing is on props and emits — the contract between components. With <script setup>, you declare them with generic type arguments, and you get autocomplete and compile-time checking on every usage:

TypeScript · typed props and emits
<script setup lang="ts">
interface Props {
  userId: number
  role?: 'admin' | 'member'
}
const props = withDefaults(defineProps<Props>(), { role: 'member' })

const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
}>()
</script>

Now passing a string where a number is expected, or emitting an event that does not exist, is a compile error instead of a bug you find in the browser. On a component reused across an app, this contract is where typing earns its keep several times over.

Typing state and refs

Refs infer their type from the initial value, so ref(0) is a number ref automatically. When the type is broader than the initial value — a ref that starts empty but holds objects — annotate it explicitly with a generic: ref<User[]>([]). This small habit prevents the ref from being locked to an overly narrow inferred type and gives you checking on everything you put in it.

Composables and stores

Typing pays off again in composables and Pinia stores — the shared logic many components consume. A typed composable's return value flows types into every caller, and a typed store gives autocomplete and safety on state and actions throughout the app. Because these are consumed widely, the types propagate value widely: one good annotation protects every usage site.

Type the boundaries first

If you adopt TypeScript incrementally, start where the payoff is largest: props, emits, composable and store signatures — the contracts between parts of your app. Internal component logic benefits less and can stay loosely typed at first. Typing the boundaries catches the mismatches that actually cause bugs, for the least ceremony.

Vue 3 with TypeScript is a genuinely good pairing thanks to <script setup>: typed props and emits for component contracts, explicit generics on refs that outgrow their initial value, and typed composables and stores that spread safety across the app. Focus the effort on the boundaries — that is where a type error would otherwise become a runtime bug — and let the types earn their keep where it matters most.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.