Master Vue.js with TypeScript using proven best practices for props, state, composables, Pinia, and more. Build scalable apps with type safety and clean code.
## Getting Started with Vue.js and TypeScript
Vue.js paired with TypeScript is a powerhouse for building robust, maintainable front-end applications. TypeScript adds static type checking, catching errors early and improving developer experience through IntelliSense and refactoring support. This guide walks you through essential best practices, from project setup to advanced patterns, ensuring your code is scalable and production-ready. We'll use Vite for fast development and include practical examples you can copy-paste into your projects.
### 1. Project Setup with Vite
Start every new Vue.js TypeScript project with Vite—it's the official build tool, lightning-fast, and supports TypeScript out of the box. Skip the headaches of manual configuration.
**Step-by-step setup:**
1. Install the Vue CLI alternative: Run `npm create vue@latest my-vue-ts-app -- --template vue-ts`.
2. Navigate to your project: `cd my-vue-ts-app`.
3. Install dependencies: `npm install`.
4. Add popular libraries for a solid foundation:
- UI: shadcn-vue ([GitHub](https://github.com/unovue/shadcn-vue))
- Icons: unplugin-icons
- Utilities: class-variance-authority (CVA) for Tailwind variants
**Pro tip:** Use the official Vue TypeScript template for pre-configured linting with ESLint and Prettier. This enforces consistent code style across your team. Example `vite.config.ts`:
```ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
})
```
Real-world application: For enterprise apps, integrate this setup with Vitest for testing: `npm install -D vitest @vitest/ui` and add to `vite.config.ts`.
### 2. Typing Components with defineProps and defineEmits
Forget generic `props: any`—use Vue 3's `defineProps` and `defineEmits` for fully typed components. This provides autocomplete and compile-time validation.
**Key rules:**
- Always use the shorthand inference syntax with `<script setup>`.
- Define props with `readonly` arrays/objects to prevent mutations.
- Use generics for complex types.
**Example: Typed Button Component**
```vue
<script setup lang="ts">
import type { ButtonVariant } from './types'
const props = defineProps<{
variant?: ButtonVariant
size?: 'sm' | 'md' | 'lg'
}>()
const emit = defineEmits<{
(e: 'click', id: number): void
}>()
</script>
<template>
<button :class="['btn', props.variant]" @click="emit('click', 123)">
{{ $slots.default?.() }}
</button>
</template>
```
**Adding value:** For validation, wrap with `withDefaults`:
```ts
const props = withDefaults(defineProps<{
label?: string
}>(), {
label: 'Click me'
})
```
This pattern scales to forms, modals, and reusable UI kits. In a dashboard app, type all props to avoid runtime surprises.
### 3. Reactive State: ref, reactive, and computed
Vue's reactivity system shines with TypeScript. Use `ref` for primitives/objects, `reactive` for objects only, and `computed` for derived state.
**Best practices:**
- Prefer `ref` with `!` non-null assertion for known values.
- Use `readonly` for props-derived refs.
- Chain `computed` for performance.
**Practical example: Counter with computed total**
```vue
<script setup lang="ts">
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
</script>
<template>
<div>
Count: {{ count }} | Double: {{ double }}
<button @click="increment">+</button>
</div>
</template>
```
**Real-world tip:** In lists, use `computed` for filtered/sorted data: `const filteredUsers = computed(() => users.value.filter(u => u.active))`. This keeps your template clean and reactive.
### 4. Composables: Reusable Logic
Composables are Vue's answer to React hooks—pure functions returning reactive state. They're perfect for custom logic like API fetches or form handling.
**Guidelines:**
- Name with `use` prefix (e.g., `useFetch`).
- Export refs directly.
- Handle lifecycle with `onMounted`/`onUnmounted`.
**Example: useCounter Composable**
```ts
// composables/useCounter.ts
import { ref, type Ref } from 'vue'
export function useCounter(initial: number = 0): { count: Ref<number>, increment: () => void } {
const count = ref(initial)
const increment = () => count.value++
return { count, increment }
}
```
Usage in component:
```vue
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter'
const { count, increment } = useCounter(10)
</script>
```
**Enhancement:** Integrate VueUse ([GitHub](https://github.com/vuejs/vueuse)) for 200+ battle-tested composables like `useStorage` or `useDebounce`. For a todo app, create `useTodos` with persistence.
### 5. State Management with Pinia
Pinia is the modern store solution—lightweight, TypeScript-first, and devtools-friendly. Ditch Vuex.
**Setup:** `npm install pinia`, add to `main.ts`:
```ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(createPinia())
```
**Store example: User Store**
```ts
// stores/user.ts
import { defineStore } from 'pinia'
import type { User } from '@/types'
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const fetchUser = async (id: string) => {
user.value = await api.getUser(id)
}
return { user, fetchUser }
})
```
**Pro patterns:**
- Use `defineStore('id', () => {})` for setup stores (recommended).
- `$subscribe` for side effects.
- Plugins for persistence.
In e-commerce apps, manage cart state here for global access.
### 6. Vue Router with Typed Routes
Type-safe routing prevents typos and 404s. Use `vue-router` with path-to-regexp types.
**Install:** `npm install vue-router@4`.
**Typed router setup in `router/index.ts`:**
```ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{ path: '/users/:id', component: UserView }
]
export const router = createRouter({
history: createWebHistory(),
routes
})
```
**Infer types:** Use `RouteLocationRaw` and `RouterOptions` for full safety.
**Navigation guards:** Protect routes with `beforeEach`:
```ts
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isLoggedIn()) next('/login')
else next()
})
```
### 7. Advanced Patterns: defineModel and Suspense
**defineModel (Vue 3.4+):** Simplifies v-model on components.
```vue
<script setup lang="ts">
const modelValue = defineModel<boolean>({ required: true })
</script>
<template>
<input v-model="modelValue" type="checkbox" />
</template>
```
Reference: [RFC](https://github.com/vuejs/rfcs/blob/main/active-rfcs/0040-define-model.md)
**Suspense for async components:** Wrap lazy-loaded parts:
```vue
<Suspense>
<template #default><AsyncComponent /></template>
<template #fallback>Loading...</template>
</Suspense>
```
### 8. Linting and Tooling
Enforce best practices:
- ESLint: `vue/eslint-config-typescript`.
- Prettier for formatting.
- `@vue/eslint-config-prettier`.
`.eslintrc.js`:
```js
module.exports = {
extends: [
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier'
]
}
```
### Final Tips for Production
- Use `unplugin-vue-components` for auto-imports.
- Environment variables: `VITE_API_URL`.
- Testing: Vitest + Vue Test Utils.
- Deployment: Vite build to static hosting.
Follow these practices, and your Vue.js TypeScript apps will be type-safe, performant, and a joy to maintain. Experiment in a sandbox project today!
<div style="text-align: center; margin-top: 2rem;">
<a href="https://cursor.directory/vuejs-typescript-best-practices" target="_blank" rel="noopener noreferrer" class="view-full-resource-btn" style="display: inline-block; background-color: #f97316; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; transition: background-color 0.2s;">View Full Resource</a>
</div>