## Introduction to Vue.js and TypeScript Synergy
Combining Vue.js with TypeScript elevates frontend development by introducing static type checking, improved IDE support, and reduced runtime errors. This guide dives deep into recommended practices drawn from the Vue.js ecosystem, including insights from the official [Vue.js core repository](https://github.com/vuejs/core) and [language tools](https://github.com/vuejs/language-tools). Whether you're migrating from JavaScript or starting fresh, these patterns ensure scalable, maintainable codebases. We'll break down each area with comparisons to common pitfalls, detailed code examples, and actionable setups.
## Optimal Project Structure
A well-organized project structure is foundational for large-scale Vue.js + TypeScript applications. Unlike loosely structured JS projects, TypeScript enforces modularity through explicit types and imports.
### Key Folders and Conventions
- **src/**: Root for all source code.
- **components/**: Reusable UI components, grouped by feature (e.g., `components/ui/`, `components/features/`).
- **composables/**: Custom hooks for logic reuse (more on this later).
- **stores/**: Pinia store modules.
- **types/**: Shared TypeScript definitions (e.g., interfaces for API responses).
- **router/**: Vue Router configuration.
- **views/**: Page-level components.
- **utils/**: Helper functions with types.
This mirrors Vue's official recommendations and contrasts with flat structures that lead to import hell in monorepos. For instance, in a e-commerce app, place product-related components under `components/features/products/` to colocate logic and UI.
```
src/
├── components/
│ ├── ui/
│ └── features/
├── composables/
├── stores/
├── types/
├── router/
├── views/
└── utils/
```
## Comprehensive Type Definitions
TypeScript shines in Vue by defining props, state, and events explicitly. Leverage Vue's built-in `defineProps` and `defineEmits` with generics for inference, as outlined in [Vue's RFC for props/emit](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-props-emit-define.md).
### Global Types Setup
Create `shims-vue.d.ts` for Vue component typing:
```typescript
// src/shims-vue.d.ts
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
```
For environment variables, extend `ImportMeta`:
```typescript
// vite-env.d.ts
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
```
This prevents `any` proliferation, unlike plain JS where types are implicit and errors surface late.
## Typed Component Props and Emits
Modern Vue 3 favors `defineProps` and `defineEmits` over Options API for better type inference. Compare to legacy `props: {}` which lacks runtime validation.
### Example: Typed Button Component
```vue
<script setup lang="ts">
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}
const props = withDefaults(defineProps<ButtonProps>(), {
variant: 'primary',
size: 'md',
disabled: false,
});
const emit = defineEmits<{
(e: 'click', id: number): void;
(e: 'hover'): void;
}>();
</script>
<template>
<button
:class="[variant, size]"
:disabled="disabled"
@click="$event => emit('click', 123)"
@mouseenter="emit('hover')"
>
{{ $slots.default?.() }}
</button>
</template>
```
This setup provides full IntelliSense and catches mismatches early. Reference the [updated RFC pull](https://github.com/vuejs/rfcs/pull/281) for advanced emit typing.
## Reusable Composables with Types
Composables encapsulate reactive logic, outperforming mixins by avoiding naming conflicts. Type them rigorously for composability.
### useCounter Composable
```typescript
// composables/useCounter.ts
import { ref, computed, type Ref } from 'vue';
export function useCounter(initialValue: number = 0) {
const count = ref(initialValue);
const double = computed(() => count.value * 2);
const increment = () => count.value++;
const decrement = () => count.value--;
return { count, double, increment, decrement };
}
```
Usage in component:
```vue
<script setup lang="ts">
const { count, double, increment } = useCounter(5);
</script>
```
In real-world apps like dashboards, chain composables (e.g., `useFetch` + `useCounter`) for metrics tracking, adding error handling types.
## Pinia Stores for State Management
Pinia is Vue's recommended store, fully typed with TypeScript. It surpasses Vuex in simplicity and plugin support.
### Typed Store Definition
```typescript
// stores/counter.ts
import { defineStore } from 'pinia';
interface CounterState {
count: number;
}
export const useCounterStore = defineStore('counter', {
state: (): CounterState => ({ count: 0 }),
getters: {
double: (state): number => state.count * 2,
},
actions: {
increment() {
this.count++;
},
},
});
```
Access via `const counterStore = useCounterStore()`; TypeScript ensures action safety. For modules, use namespaces like `useUserStore`.
## Integrating VueUse Utilities
[VueUse](https://vueuse.org/) offers 200+ typed composables. Install via `npm i @vueuse/core` and import selectively to avoid bundle bloat.
Example: Auto-responsive sidebar
```vue
<script setup>
import { useElementBounding, useResizeObserver } from '@vueuse/core';
const el = ref<HTMLElement>();
const { height } = useElementBounding(el);
useResizeObserver(el, () => {/* handle resize */});
</script>
```
This adds polish to apps, like useMotion for animations or useStorage for persistence.
## Vite Configuration Optimizations
Vite's speed pairs perfectly with Vue + TS. Customize `vite.config.ts`:
```typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
});
```
Aliases reduce import verbosity (e.g., `import Comp from '@/components/Comp.vue'`). Enable TS path mapping for composables.
## ESLint and TypeScript Linting
Enforce consistency with [@antfu/eslint-config](https://github.com/antfu/eslint-config) and [@vue/eslint-config-typescript](https://github.com/vuejs/eslint-config-typescript).
`.eslintrc.js`:
```javascript
module.exports = {
extends: [
'plugin:vue/vue3-essential',
'@vue/eslint-config-typescript',
'@antfu',
],
};
```
This catches TS errors in JS contexts and formats via Prettier. Compare to unlinted code: fewer bugs in props validation.
## Robust Testing with Vitest
Use [Vitest](https://github.com/vitest-dev/vitest) for fast, Vue-aware tests alongside [@vue/test-utils](https://github.com/vuejs/test-utils).
Setup in `vite.config.ts`:
```typescript
test: {
environment: 'jsdom',
},
```
Component test:
```typescript
// Counter.test.ts
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
test('increments counter', () => {
const wrapper = mount(Counter);
expect(wrapper.text()).toContain('0');
// simulate click
});
```
Aim for 80% coverage; mocks typed APIs for reliability.
## Conclusion
Adopting these practices transforms Vue.js + TypeScript into a powerhouse for enterprise apps. Experiment in a [Vue starter](https://github.com/vuejs/vue) and scale confidently.
<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>