Have you ever spent hours debugging because a Vue component received a prop with the wrong type? Or a Pinia store changed structure without warning and your app exploded in production? At Meteora Web, we've been building Vue applications for years, and we've seen the cost of missing types: lost hours, sneaky bugs, unhappy clients. TypeScript isn't a trend — it's a guarantee that your code behaves as expected. In this guide, we show you how to bring TypeScript into your Vue 3 components and Pinia stores, with ready-to-copy examples.
Why TypeScript with Vue saves time and money?
TypeScript adds a static type layer that catches errors before deployment. If a prop receives a number instead of a string, or a store returns an object with a missing property, TypeScript flags it during development, not in production. For a small or medium business, this means fewer bugs, less debugging time, and more confidence in releases. We apply it on every Vue project — from small internal tools to multi-client platforms. The return is immediate: code that is easier to maintain and evolve.
Common errors TypeScript prevents
- Prop passed with wrong type (e.g., string instead of number) — TypeScript catches it instantly.
- Unsafed emit: if a child component emits an event with a wrong payload, the parent receives it incorrectly. TypeScript types the emit.
- Fragile store structure: changing a field in a Pinia store without updating components used to cause silent errors. With TypeScript the compiler yells.
- Untyped refs and reactive: if you write
const x = ref([]), TypeScript infersRef<never[]>and restricts operations.
How to type Vue 3 component props with Composition API?
The Composition API allows writing logic in setup functions, but props must be declared and typed. There are two approaches: using an explicit interface or inline declaration. We prefer the first — more readable and reusable.
Sponsored Protocol
Interface approach
// components/UserCard.vue
<script setup lang="ts">
interface Props {
name: string;
age: number;
isActive?: boolean; // optional
}
const props = defineProps<Props>();
// Now props.name is string, props.age is number, props.isActive is boolean | undefined
</script>
With defineProps<Props>(), TypeScript checks every usage. If elsewhere you pass name={42}, the compiler complains. Note: defineProps with generics works only with <script setup>. If using Options API, you must fall back to PropType.
Sponsored Protocol
Props with default values
interface Props {
title: string;
count?: number;
}
const props = withDefaults(defineProps<Props>(), {
count: 0
});
// count is always number even if not passed
How to type emits with TypeScript?
Outgoing events from a component must also be typed to prevent the parent from receiving malformed data. Declare them with defineEmits.
// components/UserForm.vue
<script setup lang="ts">
interface Emits {
(e: 'submit', data: { name: string; email: string }): void;
(e: 'cancel'): void;
}
const emit = defineEmits<Emits>();
// Usage
emit('submit', { name: 'Mario', email: 'mario@example.com' }); // OK
emit('submit', { name: 123 }); // ERROR: name must be string
</script>
Alternative: with TypeScript 5.x you can use defineEmits<{ submit: [data: { name: string; email: string }]; cancel: [] }>(). Choose whichever you like.
How to type ref, reactive and computed?
We often see const items = ref([]) — TypeScript infers Ref<never[]>, and then items.value.push('test') fails because it expects never. Always declare the type explicitly.
Sponsored Protocol
Typed ref
import { ref } from 'vue';
const counter = ref<number>(0); // Ref<number>
const users = ref<User[]>([]); // User is an interface
interface User {
id: number;
name: string;
}
Typed reactive
reactive() infers type from the passed object. To explicitly type, use an interface.
interface FormState {
email: string;
password: string;
remember: boolean;
}
const form = reactive<FormState>({
email: '',
password: '',
remember: false
});
Typed computed
The type is inferred automatically from the return value. But if you want to force it (useful for generics), use the generic parameter.
const double = computed<number>(() => counter.value * 2);
How to type a Pinia store with TypeScript?
Pinia is natively TypeScript-friendly. The best approach is using the Setup Store (or Option Store) with explicit types. The Setup Store integrates perfectly with Composition API.
Typed Setup Store
// stores/cart.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
interface CartItem {
productId: number;
name: string;
price: number;
quantity: number;
}
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([]);
const total = computed(() => {
return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0);
});
function addItem(item: CartItem) {
const existing = items.value.find(i => i.productId === item.productId);
if (existing) {
existing.quantity += item.quantity;
} else {
items.value.push(item);
}
}
return { items, total, addItem };
});
In a component:
Sponsored Protocol
const store = useCartStore();
store.addItem({ productId: 1, name: 'T-shirt', price: 25, quantity: 2 }); // OK
store.addItem({ productId: 'abc' }); // ERROR
Typing getters and actions with advanced generics
If you have getters returning types dependent on state, TypeScript infers them automatically. You can also export a type for the whole store.
export type CartStore = ReturnType<typeof useCartStore>;
Handy for passing the store as a prop or via inject.
Sponsored Protocol
What to do next: checklist for adding TypeScript to an existing Vue project
- Install TypeScript and Vue plugins:
npm install -D typescript @vue/tsconfigand configuretsconfig.jsonas per official guide. - Rename files: from
.jsto.tsand Vue components withlang="ts"on script. - Type props first: start with the most used components. Use
defineProps<Props>(). - Type Pinia stores: define interfaces for data and add types to ref and reactive.
- Enable strict mode: in
tsconfig.jsonset"strict": truefor maximum safety. - Run
vue-tsc --noEmit: to check for type errors before build. - Integrate in CI: add a type-checking job to prevent untyped code from reaching production.
We, at Meteora Web, followed these steps on over a dozen projects — the result is more stable code and faster code reviews. TypeScript is not overhead; it's an investment that pays off at the first bug-free deploy.
For a deeper dive into the Vue 3 ecosystem with Composition API, check our pillar guide on Vue.js 3 and Composition API.
External resource: Official Vue TypeScript documentation.