
Vue Best Practices
- 2 installs
- 404 repo stars
- Updated August 5, 2026
- aiskillstore/marketplace
vue-best-practices is a skill providing Vue 3 and TypeScript typing rules for vue-tsc and Volar, covering prop extraction, strict templates, and Volar upgrades.
About
vue-best-practices is a rules-based reference for correct Vue 3 and TypeScript typing patterns when writing, reviewing, or refactoring Vue components. A developer uses it for issues like extracting component prop types, typing fallthrough attributes, enabling strict template checking, or fixing Volar and module-resolution errors. It maps common Vue typing keywords to specific rule files.
- Vue 3 + TypeScript typing rules for vue-tsc and Volar
- Covers prop extraction, wrapper components, and strict template checking
- Includes Volar 3.0 upgrade and module-resolution fixes
Vue Best Practices by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,863 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
vue-best-practices capabilities & compatibility
- Capabilities
- code review · refactoring · frontend
- Use cases
- frontend · refactoring · code review
- IDEs
- vscode · cursor ide
What vue-best-practices says it does
Vue 3 and Vue.js best practices for TypeScript, vue-tsc, and Volar.
Catch undefined components in templates
Fix Volar 3.0 upgrade issues
npx skills add https://github.com/aiskillstore/marketplace --skill vue-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 404 |
| Last updated | August 5, 2026 |
| Repository | aiskillstore/marketplace ↗ |
What it does
Apply correct Vue 3 and TypeScript typing patterns when writing or refactoring Vue components.
Who is it for?
Getting Vue 3 component typing right with vue-tsc and Volar
Skip if: General Vue architecture or non-TypeScript Vue projects
When should I use this skill?
Writing, reviewing, or refactoring Vue components with TypeScript typing questions
What you get
Correctly typed Vue components that pass vue-tsc strict template checks
- Rule-based typing fixes for Vue components
By the numbers
- 12 capability rules plus 2 efficiency rules
Files
Capability Rules
| Rule | Keywords | Description |
|---|---|---|
| extract-component-props | get props type, wrapper component, extend props, inherit props, ComponentProps | Extract types from .vue components |
| vue-tsc-strict-templates | undefined component, template error, strictTemplates | Catch undefined components in templates |
| fallthrough-attributes | fallthrough, $attrs, wrapper component | Type-check fallthrough attributes |
| strict-css-modules | css modules, $style, typo | Catch CSS module class typos |
| data-attributes-config | data-*, strictTemplates, attribute | Allow data-* attributes |
| volar-3-breaking-changes | volar, vue-language-server, editor | Fix Volar 3.0 upgrade issues |
| module-resolution-bundler | cannot find module, @vue/tsconfig, moduleResolution | Fix module resolution errors |
| define-model-update-event | defineModel, update event, undefined | Fix model update errors |
| with-defaults-union-types | withDefaults, union type, default | Fix union type defaults |
| deep-watch-numeric | watch, deep, array, Vue 3.5 | Efficient array watching |
| vue-directive-comments | @vue-ignore, @vue-skip, template | Control template type checking |
| vue-router-typed-params | route params, typed router, unplugin | Fix route params typing |
Efficiency Rules
| Rule | Keywords | Description |
|---|---|---|
| hmr-vue-ssr | hmr, ssr, hot reload | Fix HMR in SSR apps |
| pinia-store-mocking | pinia, mock, vitest, store | Mock Pinia stores |
Reference
MIT License
Copyright (c) 2025 hyf0
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Fix Slow Save Times with Code Actions Setting
Impact: HIGH - fixes 30-60 second save delays in large Vue projects
In large Vue projects, saving files can take 30-60+ seconds due to VSCode's code actions triggering expensive TypeScript state synchronization.
Problem
Symptoms:
- Save operation takes 30+ seconds
- Editor becomes unresponsive during save
- CPU spikes when saving Vue files
- Happens more in larger projects
Root Cause
VSCode emits document change events multiple times during save cycles. Each event triggers Volar to synchronize with TypeScript, causing expensive re-computation.
Solution
Disable code actions or limit their timeout:
Option 1: Disable code actions (fastest)
// .vscode/settings.json
{
"vue.codeActions.enabled": false
}Option 2: Limit code action time
// .vscode/settings.json
{
"vue.codeActions.savingTimeLimit": 1000
}Option 3: Disable specific code actions
// .vscode/settings.json
{
"vue.codeActions.enabled": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
}
}VSCode Version Requirement
VSCode 1.81.0+ includes fixes that reduce save time issues. Upgrade if using an older version.
Additional Optimizations
// .vscode/settings.json
{
"vue.codeActions.enabled": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {},
"[vue]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "Vue.volar"
}
}Reference
Allow Data Attributes with Strict Templates
Impact: MEDIUM - fixes data-testid and data-* attribute errors in strict mode
With strictTemplates enabled, data-* attributes on components cause type errors. Use the dataAttributes option to allow specific patterns.
Problem
<template>
<!-- Error: Property 'data-testid' does not exist on type... -->
<MyComponent data-testid="submit-button" />
<!-- Error: Property 'data-cy' does not exist on type... -->
<MyComponent data-cy="login-form" />
</template>Solution
Configure dataAttributes to allow specific patterns:
// tsconfig.json or tsconfig.app.json
{
"vueCompilerOptions": {
"strictTemplates": true,
"dataAttributes": ["data-*"]
}
}Now all data-* attributes are allowed on any component.
Specific Patterns
You can be more selective:
{
"vueCompilerOptions": {
"dataAttributes": [
"data-testid",
"data-cy",
"data-test-*"
]
}
}This only allows the specified patterns, not all data attributes.
Common Testing Attributes
For testing libraries, allow their specific attributes:
| Library | Attribute | Pattern |
|---|---|---|
| Testing Library | data-testid | "data-testid" |
| Cypress | data-cy | "data-cy" |
| Playwright | data-testid | "data-testid" |
| Generic | All data attributes | "data-*" |
Reference
Vue 3.5+ Deep Watch Numeric Depth
Impact: MEDIUM - enables efficient array mutation watching with numeric deep option
Vue 3.5 introduced deep: number for watch depth control. This allows watching array mutations without the performance cost of deep traversal.
Symptoms
- Array mutations not triggering watch callback
- Deep watch causing performance issues on large nested objects
- Unaware of new Vue 3.5 feature
Note: TypeScript error "Type 'number' is not assignable to type 'boolean'" no longer occurs with Vue 3.5+ and current TypeScript versions. The types now correctly support numeric deep values.The Feature
// Vue 3.5+ only
watch(items, (newVal) => {
// Triggered on array mutations (push, pop, splice, etc.)
}, { deep: 1 })| deep value | Behavior |
|---|---|
true | Full recursive traversal (original behavior) |
false | Only reference changes |
1 | One level deep - array mutations, not nested objects |
2 | Two levels deep |
n | N levels deep |
Fix
Step 1: Ensure Vue 3.5+
npm install vue@^3.5.0Step 2: Use numeric depth
import { watch, ref } from 'vue'
const items = ref([{ id: 1, data: { nested: 'value' } }])
// Watch array mutations only (push, pop, etc.)
watch(items, (newItems) => {
console.log('Array mutated')
}, { deep: 1 })
// Won't trigger on: items.value[0].data.nested = 'new'
// Will trigger on: items.value.push(newItem)Performance Comparison
const largeNestedData = ref({ /* deeply nested structure */ })
// SLOW - traverses entire structure
watch(largeNestedData, handler, { deep: true })
// FAST - only watches top-level changes
watch(largeNestedData, handler, { deep: 1 })
// FASTEST - only reference changes
watch(largeNestedData, handler, { deep: false })Alternative: watchEffect for Selective Tracking
// Only tracks properties actually accessed
watchEffect(() => {
// Only re-runs when items.value.length or first item changes
console.log(items.value.length, items.value[0]?.id)
})TypeScript Note
If TypeScript complains about numeric deep, ensure: 1. Vue version is 3.5+ 2. TypeScript version is current (types are included with vue package) 3. tsconfig targets correct node_modules types
Reference
defineModel Fires Update Event with Undefined
Impact: MEDIUM - fixes runtime errors from unexpected undefined in model updates
Version Note: This issue may be resolved in Vue 3.5+. Testing with Vue 3.5.26 could not reproduce the double emission with undefined. If you're on Vue 3.5+, verify the issue exists in your specific scenario before applying workarounds.Components using defineModel may fire the @update:model-value event with undefined in certain edge cases. TypeScript types don't always reflect this behavior, potentially causing runtime errors when the parent expects a non-nullable value.
Symptoms
- Parent component receives
undefinedunexpectedly - Runtime error: "Cannot read property of undefined"
- Type mismatch between expected
Tand receivedT | undefined - Issue appears when clearing/resetting the model value
Root Cause
defineModel returns Ref<T | undefined> by default, even when T is non-nullable. The update event can fire with undefined when:
- Component unmounts
- Model is explicitly cleared
- Internal state resets
Fix
Option 1: Use required option (Vue 3.5+)
// Returns Ref<Item> instead of Ref<Item | undefined>
const model = defineModel<Item>({ required: true })Option 2: Type parent handler to accept undefined
<template>
<MyComponent
v-model="item"
@update:model-value="handleUpdate"
/>
</template>
<script setup lang="ts">
// Handle both value and undefined
const handleUpdate = (value: Item | undefined) => {
if (value !== undefined) {
item.value = value
}
}
</script>Option 3: Use default value in defineModel
const model = defineModel<string>({ default: '' })Type Declaration Pattern
// In child component
interface Props {
modelValue: Item
}
const model = defineModel<Item>({ required: true })
// Emits will be typed as (value: Item) not (value: Item | undefined)Reference
Duplicate Vue Plugin Detection
Impact: MEDIUM - fixes cryptic build errors from Vue plugin registered twice
When using Vite's JavaScript API, if the Vue plugin is loaded in vite.config.js and specified again in inlineConfig, it gets registered twice, causing cryptic build errors.
Symptoms
- Build produces unexpected output or fails silently
- "Cannot read property of undefined" during build
- Different build behavior between CLI and JavaScript API
- Vue components render incorrectly after build
Root Cause
Vite doesn't deduplicate plugins by name when merging configs. The Vue plugin's internal state gets corrupted when registered twice.
Fix
Option 1: Use configFile: false with inline plugins
import { build } from 'vite'
import vue from '@vitejs/plugin-vue'
await build({
configFile: false, // Don't load vite.config.js
plugins: [vue()],
// ... rest of config
})Option 2: Don't specify plugins in inlineConfig
// vite.config.js already has vue plugin
import { build } from 'vite'
await build({
// Don't add vue plugin here - it's in vite.config.js
root: './src',
build: { outDir: '../dist' }
})Option 3: Filter out Vue plugin before merging
import { build, loadConfigFromFile } from 'vite'
import vue from '@vitejs/plugin-vue'
const { config } = await loadConfigFromFile({ command: 'build', mode: 'production' })
// Remove existing Vue plugin
const filteredPlugins = config.plugins?.filter(
p => !p || (Array.isArray(p) ? false : p.name !== 'vite:vue')
) || []
await build({
...config,
plugins: [...filteredPlugins, vue({ /* your options */ })]
})Detection Script
Add this to debug plugin registration:
// vite.config.ts
export default defineConfig({
plugins: [
vue(),
{
name: 'debug-plugins',
configResolved(config) {
const vuePlugins = config.plugins.filter(p => p.name?.includes('vue'))
if (vuePlugins.length > 1) {
console.warn('WARNING: Multiple Vue plugins detected:', vuePlugins.map(p => p.name))
}
}
}
]
})Common Scenarios
| Scenario | Solution |
|---|---|
Using vite.createServer() | Use configFile: false |
| Build script with custom config | Don't duplicate plugins |
| Monorepo with shared config | Check for plugin inheritance |
Reference
Extract Component Props
Impact: HIGH - extract props, emits, slots types from .vue components
Use vue-component-type-helpers to extract types from .vue components:
npm install -D vue-component-type-helpersimport type { ComponentProps, ComponentEmit, ComponentSlots, ComponentExposed } from 'vue-component-type-helpers'
import MyButton from './MyButton.vue'
type Props = ComponentProps<typeof MyButton>
type Emits = ComponentEmit<typeof MyButton>
type Slots = ComponentSlots<typeof MyButton>
type Exposed = ComponentExposed<typeof MyButton>Wrapper Component Pattern
import type { ComponentProps } from 'vue-component-type-helpers'
import BaseButton from './BaseButton.vue'
type BaseProps = ComponentProps<typeof BaseButton>
interface Props extends BaseProps {
size: 'sm' | 'md' | 'lg'
}
defineProps<Props>()Do NOT Use
// ❌ Includes Vue internal properties (onUpdate:*, class, style, etc.)
type Props = InstanceType<typeof MyButton>['$props']Note
Vue's built-in ExtractPropTypes is for runtime props objects (props: { foo: String }), not for .vue components.
Reference
Enable Fallthrough Attributes Type Checking
Impact: MEDIUM - enables type-aware attribute forwarding in component libraries
When building component libraries with wrapper components, enable fallthroughAttributes to get IDE autocomplete for attributes that will be forwarded to child elements.
What It Does
Wrapper components that pass attributes to child elements can benefit from type-aware completion:
<!-- MyButton.vue - wrapper around native button -->
<template>
<button v-bind="$attrs"><slot /></button>
</template>Solution
Enable fallthroughAttributes in your tsconfig:
// tsconfig.json or tsconfig.app.json
{
"vueCompilerOptions": {
"fallthroughAttributes": true
}
}How It Works
When fallthroughAttributes: true:
- Vue Language Server analyzes which element receives
$attrs - IDE autocomplete suggests valid attributes for the target element
- Helps developers discover available attributes
Note: This primarily enables IDE autocomplete for valid fallthrough attributes. It does NOT reject invalid attributes as type errors - arbitrary attributes are still allowed.
Related Options
Combine with strictTemplates for comprehensive checking:
{
"vueCompilerOptions": {
"strictTemplates": true,
"fallthroughAttributes": true
}
}Reference
HMR Debugging for Vue SSR
Impact: MEDIUM - fixes Hot Module Replacement breaking in Vue SSR applications
Hot Module Replacement breaks when modifying Vue component <script setup> sections in SSR applications. Changes cause errors instead of smooth updates, requiring full page reloads.
Symptoms
- HMR works for
<template>changes but breaks for<script setup> - "Cannot read property of undefined" after saving
- Full page reload required after script changes
- HMR works in dev:client but not dev:ssr
Root Cause
SSR mode has a different transformation pipeline. The Vue plugin's HMR boundary detection doesn't handle SSR modules the same way as client modules.
Fix
Step 1: Ensure correct SSR plugin configuration
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
ssr: {
// Don't externalize these for HMR to work
noExternal: ['vue', '@vue/runtime-core', '@vue/runtime-dom']
}
})Step 2: Configure dev server for SSR HMR
// server.ts
import { createServer } from 'vite'
const vite = await createServer({
server: { middlewareMode: true },
appType: 'custom'
})
// Use vite.ssrLoadModule for server-side imports
const { render } = await vite.ssrLoadModule('/src/entry-server.ts')
// Handle HMR
vite.watcher.on('change', async (file) => {
if (file.endsWith('.vue')) {
// Invalidate the module
const mod = vite.moduleGraph.getModuleById(file)
if (mod) {
vite.moduleGraph.invalidateModule(mod)
}
}
})Step 3: Add HMR acceptance in entry-server
// entry-server.ts
import { createApp } from './main'
export async function render(url: string) {
const app = createApp()
// ... render logic
}
// Accept HMR updates
if (import.meta.hot) {
import.meta.hot.accept()
}Framework-Specific Solutions
Nuxt 3
HMR should work out of the box. If not:
rm -rf .nuxt node_modules/.vite
npm install
npm run devVite SSR Template
Ensure you're using the latest @vitejs/plugin-vue:
npm install @vitejs/plugin-vue@latestDebugging
Enable verbose HMR logging:
// vite.config.ts
export default defineConfig({
server: {
hmr: {
overlay: true
}
},
logLevel: 'info' // Shows HMR updates
})Known Limitations
- HMR for
<script>(not<script setup>) may require full reload - SSR components with external dependencies may not hot-reload
- State is not preserved for SSR components (expected behavior)
Reference
moduleResolution Bundler Migration Issues
Impact: HIGH - fixes "Cannot find module" errors after @vue/tsconfig upgrade
Recent versions of @vue/tsconfig changed moduleResolution from "node" to "bundler". This can break existing projects with errors like "Cannot find module 'vue'" or issues with resolveJsonModule.
Symptoms
Cannot find module 'vue'or other packagesOption '--resolveJsonModule' cannot be specified without 'node' module resolution- Errors appear after updating
@vue/tsconfig - Some third-party packages no longer resolve
Root Cause
moduleResolution: "bundler" requires: 1. TypeScript 5.0+ 2. Packages to have proper exports field in package.json 3. Different resolution rules than Node.js classic resolution
Fix
Option 1: Ensure TypeScript 5.0+ everywhere
npm install -D typescript@^5.0.0In monorepos, ALL packages must use TypeScript 5.0+.
Option 2: Add compatibility workaround
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"resolvePackageJsonExports": false
}
}Setting resolvePackageJsonExports: false restores compatibility with packages that don't have proper exports.
Option 3: Revert to Node resolution
{
"compilerOptions": {
"moduleResolution": "node"
}
}Which Packages Break?
Packages break if they:
- Lack
exportsfield in package.json - Have incorrect
exportsconfiguration - Rely on Node.js-specific resolution behavior
Diagnosis
# Check which resolution is being used
cat tsconfig.json | grep moduleResolution
# Test if a specific module resolves
npx tsc --traceResolution 2>&1 | grep "module-name"Reference
Mocking Pinia Stores with Vitest
Impact: HIGH - properly mocks Pinia stores in component tests
Developers struggle to properly mock Pinia stores: createTestingPinia requires explicit createSpy configuration, and "injection Symbol(pinia) not found" errors occur without proper setup.
Important (@pinia/testing 1.0+): ThecreateSpyoption is REQUIRED, not optional. Omitting it throws an error: "You must configure thecreateSpyoption."
Symptoms
- "injection Symbol(pinia) not found" error
- "You must configure the
createSpyoption" error - Actions not properly mocked
- Store state not reset between tests
Fix
Pattern 1: Basic setup with createTestingPinia
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { vi } from 'vitest'
import MyComponent from './MyComponent.vue'
import { useCounterStore } from '@/stores/counter'
test('component uses store', async () => {
const wrapper = mount(MyComponent, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn, // REQUIRED in @pinia/testing 1.0+
initialState: {
counter: { count: 10 } // Set initial state
}
})
]
}
})
// Get the store instance AFTER mounting
const store = useCounterStore()
// Actions are automatically stubbed
await wrapper.find('button').trigger('click')
expect(store.increment).toHaveBeenCalled()
})Pattern 2: Customize action behavior
test('component handles async action', async () => {
const wrapper = mount(MyComponent, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn,
stubActions: false // Don't stub, use real actions
})
]
}
})
const store = useCounterStore()
// Override specific action
store.fetchData = vi.fn().mockResolvedValue({ items: [] })
await wrapper.find('.load-button').trigger('click')
expect(store.fetchData).toHaveBeenCalled()
})Pattern 3: Testing store directly
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
test('increments count', () => {
const store = useCounterStore()
expect(store.count).toBe(0)
store.increment()
expect(store.count).toBe(1)
})
})Setup Store with Vitest
// stores/counter.ts - Setup store syntax
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
// Test file
test('setup store works', async () => {
const pinia = createTestingPinia({
createSpy: vi.fn,
initialState: {
counter: { count: 5 }
}
})
const wrapper = mount(MyComponent, {
global: { plugins: [pinia] }
})
const store = useCounterStore()
expect(store.count).toBe(5)
expect(store.doubleCount).toBe(10)
})Reset Between Tests
describe('Store Tests', () => {
let pinia: Pinia
beforeEach(() => {
pinia = createTestingPinia({
createSpy: vi.fn
})
})
afterEach(() => {
vi.clearAllMocks()
})
test('test 1', () => { /* ... */ })
test('test 2', () => { /* ... */ })
})Reference
JSDoc Documentation for Script Setup Components
Impact: MEDIUM - enables proper documentation for composition API components
<script setup> doesn't have an obvious place to attach JSDoc comments for the component itself. Use a dual-script pattern.
Problem
Incorrect:
<script setup lang="ts">
/**
* This comment doesn't appear in IDE hover or docs
* @component
*/
import { ref } from 'vue'
const count = ref(0)
</script>JSDoc comments inside <script setup> don't attach to the component export because there's no explicit export statement.
Solution
Use both <script> and <script setup> blocks:
Correct:
<script lang="ts">
/**
* A counter component that displays and increments a value.
*
* @example
* ```vue
* <Counter :initial="5" @update="handleUpdate" />
* ```
*
* @component
*/
export default {}
</script>
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
/** Starting value for the counter */
initial?: number
}>()
const emit = defineEmits<{
/** Emitted when counter value changes */
update: [value: number]
}>()
const count = ref(props.initial ?? 0)
</script>How It Works
- The regular
<script>block's default export is merged with<script setup> - JSDoc on
export default {}attaches to the component - Props and emits JSDoc in
<script setup>still work normally
What Gets Documented
| Location | Shows In |
|---|---|
export default {} JSDoc | Component import hover |
defineProps JSDoc | Prop hover in templates |
defineEmits JSDoc | Event handler hover |
Reference
Enable Strict CSS Modules Type Checking
Impact: MEDIUM - catches typos in CSS module class names at compile time
When using CSS modules with <style module>, Vue doesn't validate class names by default. Enable strictCssModules to catch typos and undefined classes.
Problem
CSS module class name errors go undetected:
<script setup lang="ts">
// No error for typo in class name
</script>
<template>
<div :class="$style.buttn">Click me</div>
</template>
<style module>
.button {
background: blue;
}
</style>The typo buttn instead of button silently fails at runtime.
Solution
Enable strictCssModules in your tsconfig:
// tsconfig.json or tsconfig.app.json
{
"vueCompilerOptions": {
"strictCssModules": true
}
}Now $style.buttn will show a type error because buttn doesn't exist in the CSS module.
What Gets Checked
| Access | With strictCssModules |
|---|---|
$style.validClass | OK |
$style.typo | Error: Property 'typo' does not exist |
$style['dynamic'] | OK (dynamic access not checked) |
Limitations
- Only checks static property access (
$style.className) - Dynamic access (
$style[variable]) is not validated - Only works with
<style module>, not external CSS files
Reference
Volar 3.0 Breaking Changes
Impact: HIGH - fixes editor integration after Volar/vue-language-server upgrade
Volar 3.0 (vue-language-server 3.x) introduced breaking changes to the language server protocol. Editors configured for Volar 2.x will break with errors like "vue_ls doesn't work with ts_ls.. it expects vtsls".
Symptoms
vue_ls doesn't work with ts_ls- TypeScript features stop working in Vue files
- No autocomplete, type hints, or error highlighting
- Editor shows "Language server initialization failed"
Fix by Editor
VSCode
Update the "Vue - Official" extension to latest version. It manages the language server automatically.
NeoVim (nvim-lspconfig)
Option 1: Use vtsls instead of ts_ls
-- Replace ts_ls/tsserver with vtsls
require('lspconfig').vtsls.setup({})
require('lspconfig').volar.setup({})Option 2: Downgrade vue-language-server
npm install -g @vue/language-server@2.1.10JetBrains IDEs
Update to latest Vue plugin. If issues persist, disable and re-enable the Vue plugin.
What Changed in 3.0
| Feature | Volar 2.x | Volar 3.0 |
|---|---|---|
| TypeScript integration | ts_ls/tsserver | vtsls recommended (Neovim) |
| Hybrid mode | Optional | Default |
Workaround: Stay on 2.x
If upgrading is not possible:
npm install -g @vue/language-server@^2.0.0Pin in your project's package.json to prevent accidental upgrades.
Reference
Vue Template Directive Comments
Impact: HIGH - enables fine-grained control over template type checking
Vue Language Tools supports special directive comments to control type checking behavior in templates.
Available Directives
@vue-ignore
Suppress type errors for the next line:
<template>
<!-- @vue-ignore -->
<Component :prop="valueWithTypeError" />
</template>@vue-expect-error
Assert that the next line should have a type error (useful for testing):
<template>
<!-- @vue-expect-error -->
<Component :invalid-prop="value" />
</template>@vue-skip
Skip type checking for an entire block:
<template>
<!-- @vue-skip -->
<div>
<!-- Everything in here is not type-checked -->
<LegacyComponent :any="props" :go="here" />
</div>
</template>@vue-generic
Declare template-level generic types:
<template>
<!-- @vue-generic {T extends string} -->
<GenericList :items="items as T[]" />
</template>Use Cases
- Migrating legacy components with incomplete types
- Working with third-party components that have incorrect type definitions
- Temporarily suppressing errors during refactoring
- Testing that certain patterns produce expected type errors
Reference
Vue Router useRoute Params Union Type Narrowing
Impact: MEDIUM - fixes "Property does not exist" errors with typed route params
With unplugin-vue-router typed routes, route.params becomes a union of ALL page param types. TypeScript cannot narrow Record<never, never> | { id: string } properly, causing "Property 'id' does not exist" errors even on the correct page.
Symptoms
- "Property 'id' does not exist on type 'RouteParams'"
route.params.idshows asstring | undefinedeverywhere- Union type of all route params instead of specific route
- Type narrowing with
if (route.name === 'users-id')doesn't work
Root Cause
unplugin-vue-router generates a union type of all possible route params. TypeScript's control flow analysis can't narrow this union based on route name checks.
Fix
Option 1: Pass route name to useRoute (recommended)
// pages/users/[id].vue
import { useRoute } from 'vue-router/auto'
// Specify the route path for proper typing
const route = useRoute('/users/[id]')
// Now properly typed as { id: string }
console.log(route.params.id) // string, not string | undefinedOption 2: Type assertion with specific route
import { useRoute } from 'vue-router'
import type { RouteLocationNormalized } from 'vue-router/auto-routes'
const route = useRoute() as RouteLocationNormalized<'/users/[id]'>
route.params.id // Properly typedOption 3: Define route-specific param type
// In your page component
interface UserRouteParams {
id: string
}
const route = useRoute()
const { id } = route.params as UserRouteParamsRequired tsconfig Setting
Ensure moduleResolution: "bundler" for unplugin-vue-router:
{
"compilerOptions": {
"moduleResolution": "bundler"
}
}Caveat: Route Name Format
The route name matches the file path pattern:
pages/users/[id].vue→/users/[id]pages/posts/[slug]/comments.vue→/posts/[slug]/comments
Reference
Enable Strict Template Checking
Impact: HIGH - catches undefined components and props at compile time
By default, vue-tsc does not report errors for undefined components in templates. Enable strictTemplates to catch these issues during type checking.
Which tsconfig?
Add vueCompilerOptions to the tsconfig that includes Vue source files. In projects with multiple tsconfigs (like those created with create-vue), this is typically tsconfig.app.json, not the root tsconfig.json or tsconfig.node.json.
Incorrect (missing strict checking):
{
"compilerOptions": {
"strict": true
}
// vueCompilerOptions not configured - undefined components won't error
}Correct (strict template checking enabled):
{
"compilerOptions": {
"strict": true
},
"vueCompilerOptions": {
"strictTemplates": true
}
}Available Options
| Option | Default | Effect |
|---|---|---|
strictTemplates | false | Enables all checkUnknown* options below |
checkUnknownComponents | false | Error on undefined/unregistered components |
checkUnknownProps | false | Error on props not declared in component definition |
checkUnknownEvents | false | Error on events not declared via defineEmits |
checkUnknownDirectives | false | Error on unregistered custom directives |
Granular Control
If strictTemplates is too strict, enable individual checks:
{
"vueCompilerOptions": {
"checkUnknownComponents": true,
"checkUnknownProps": false
}
}Reference
withDefaults Incorrect Default with Union Types
Impact: MEDIUM - fixes spurious "Missing required prop" warning with union type props
Using withDefaults with union types like false | string may produce a Vue runtime warning "Missing required prop" even when a default is provided. The runtime value IS applied correctly, but the warning can be confusing.
Symptoms
- Vue warns "Missing required prop" despite default being set
- Warning appears only with union types like
false | string - TypeScript types are correct
- Runtime value IS correct (the default is applied)
Problematic Pattern
// This produces a spurious warning (but works at runtime)
interface Props {
value: false | string // Union type
}
const props = withDefaults(defineProps<Props>(), {
value: 'default' // Runtime value IS correct, but Vue warns about missing prop
})Fix
Option 1: Use Reactive Props Destructure (Vue 3.5+)
<script setup lang="ts">
interface Props {
value: false | string
}
// Preferred in Vue 3.5+
const { value = 'default' } = defineProps<Props>()
</script>Option 2: Use runtime declaration
<script setup lang="ts">
const props = defineProps({
value: {
type: [Boolean, String] as PropType<false | string>,
default: 'default'
}
})
</script>Option 3: Split into separate props
interface Props {
enabled: boolean
customValue?: string
}
const props = withDefaults(defineProps<Props>(), {
enabled: false,
customValue: 'default'
})Why Reactive Props Destructure Works
Vue 3.5's Reactive Props Destructure handles default values at the destructuring level, bypassing the type inference issues with withDefaults.
// The default is applied during destructuring, not type inference
const { prop = 'default' } = defineProps<{ prop?: string }>()Enable Reactive Props Destructure
This is enabled by default in Vue 3.5+. For older versions:
// vite.config.js
export default {
plugins: [
vue({
script: {
propsDestructure: true
}
})
]
}Reference
{
"schema_version": "2.0",
"meta": {
"generated_at": "2026-01-30T08:50:53.609Z",
"slug": "antfu-vue-best-practices",
"source_url": "https://github.com/antfu/skills/tree/main/skills/vue-best-practices/",
"source_ref": "main",
"model": "claude",
"analysis_version": "3.0.0",
"source_type": "community",
"content_hash": "471ebe69a80523736b02a9ac258ea2c248a40f04d6522b281aef32ca606ba645",
"tree_hash": "673bd178230eb8c0ff66bd2aa04e40fbec1b2f078bb4ba97300516f38b0ca91d"
},
"skill": {
"name": "vue-best-practices",
"description": "Vue 3 and Vue.js best practices for TypeScript, vue-tsc, and Volar. This skill should be used when writing, reviewing, or refactoring Vue components to ensure correct typing patterns. Triggers on tasks involving Vue components, props extraction, wrapper components, template type checking, or Volar configuration.",
"summary": "Best practices for Vue 3 development with TypeScript, vue-tsc, and Volar. Helps developers write type-safe Vue components.",
"icon": "📦",
"version": "1.0.0",
"author": "antfu",
"license": "MIT",
"tags": [
"vue",
"vue3",
"typescript",
"volar",
"vue-tsc"
],
"supported_tools": [
"claude",
"codex",
"claude-code"
],
"risk_factors": []
},
"security_audit": {
"risk_level": "safe",
"is_blocked": false,
"safe_to_publish": true,
"summary": "All static findings are false positives. This skill contains only markdown documentation files with code examples for Vue 3 best practices. The detected patterns (shell commands, cryptographic references, URLs) appear in code blocks and documentation links, not executable code. No security risks present.",
"critical_findings": [],
"high_findings": [
{
"title": "Static Pattern False Positive: Shell Command Detection",
"description": "The static analyzer detected 'Ruby/shell backtick execution' patterns in 247 locations across markdown files. These are JSON/TypeScript code examples in documentation, not actual shell commands. The backtick-like patterns are code fences and configuration snippets.",
"locations": [
{
"file": "rules/codeactions-save-performance.md",
"line_start": 32,
"line_end": 75
},
{
"file": "rules/data-attributes-config.md",
"line_start": 13,
"line_end": 70
},
{
"file": "rules/vue-tsc-strict-templates.md",
"line_start": 17,
"line_end": 64
}
],
"confidence": 0.95,
"confidence_reasoning": "Direct examination of markdown files confirms patterns are from JSON/TS code blocks, not executable shell commands."
},
{
"title": "Static Pattern False Positive: Weak Cryptographic Algorithm",
"description": "The static analyzer flagged 'weak cryptographic algorithm' in 45 locations. These detections are false positives triggered by the word 'md5' in documentation or by JSON structural patterns. No actual cryptographic code exists in this skill.",
"locations": [
{
"file": "SKILL.md",
"line_start": 3,
"line_end": 29
},
{
"file": "rules/codeactions-save-performance.md",
"line_start": 4,
"line_end": 60
}
],
"confidence": 0.98,
"confidence_reasoning": "Direct file inspection shows no cryptographic code. Detections appear to be triggered by text patterns in markdown documentation."
},
{
"title": "Static Pattern False Positive: Path Traversal",
"description": "The static analyzer detected 'path traversal sequence' in 2 locations. These are relative path references in code examples for file configuration, not actual path traversal vulnerabilities.",
"locations": [
{
"file": "rules/duplicate-plugin-detection.md",
"line_start": 48,
"line_end": 48
}
],
"confidence": 0.95,
"confidence_reasoning": "Path patterns appear in Vite configuration examples showing relative paths, not user input exploitation."
}
],
"medium_findings": [
{
"title": "Static Pattern False Positive: Hardcoded URLs",
"description": "The static analyzer detected 'hardcoded URL' in 33 locations. These are legitimate documentation links to Vue.js official resources, GitHub repositories, and discussion threads. URLs are expected content in documentation skills.",
"locations": [
{
"file": "SKILL.md",
"line_start": 36,
"line_end": 38
},
{
"file": "rules/codeactions-save-performance.md",
"line_start": 79,
"line_end": 79
}
],
"confidence": 0.98,
"confidence_reasoning": "All URLs are documented references to Vue.js ecosystem resources, appropriate for a best practices guide."
},
{
"title": "Static Pattern False Positive: Hidden File Access",
"description": "The static analyzer detected 'hidden file access' in 1 location. This is a VSCode settings configuration showing how to configure Vue extension settings, not actual file access.",
"locations": [
{
"file": "rules/hmr-vue-ssr.md",
"line_start": 89,
"line_end": 89
}
],
"confidence": 0.95,
"confidence_reasoning": "Pattern appears in .vscode configuration examples, not actual hidden file system operations."
}
],
"low_findings": [
{
"title": "Static Pattern False Positive: System Reconnaissance",
"description": "The static analyzer detected 'system reconnaissance' in 6 locations. These are command names like 'ls', 'cat' appearing in documentation examples for file operations or git commands.",
"locations": [
{
"file": "rules/data-attributes-config.md",
"line_start": 4,
"line_end": 11
}
],
"confidence": 0.95,
"confidence_reasoning": "Command names appear in documentation discussing command-line tools, not reconnaissance activity."
}
],
"dangerous_patterns": [],
"files_scanned": 20,
"total_lines": 1542,
"audit_model": "claude",
"audited_at": "2026-01-30T08:50:53.609Z",
"risk_factors": [],
"risk_factor_evidence": []
},
"content": {
"user_title": "Apply Vue 3 Best Practices for TypeScript",
"value_statement": "Vue 3 projects often face type checking issues, slow save times, and configuration problems. This skill provides curated solutions for Vue component typing, Volar configuration, and common Vue 3 development issues.",
"seo_keywords": [
"Vue 3 best practices",
"Vue TypeScript",
"Vue tsconfig",
"Vue components types",
"volar configuration",
"vue-tsc strict templates",
"Vue props extraction",
"Vue defineModel",
"Vue Pinia store testing",
"Claude",
"Codex",
"Claude Code"
],
"actual_capabilities": [
"Configure vue-tsc strict template checking to catch undefined components at compile time",
"Extract component prop types for wrapper components using TypeScript helpers",
"Fix Vue Volar 3.0 breaking changes and configuration issues",
"Optimize VSCode save performance for large Vue projects",
"Configure CSS modules for type-safe class name usage",
"Fix duplicate Vue plugin registration errors in Vite configurations"
],
"limitations": [
"Does not execute code or modify project files directly",
"Does not install dependencies or configure build tools automatically",
"Does not test or validate existing Vue component implementations",
"Only provides guidance documentation, not automated fixes"
],
"use_cases": [
{
"title": "Fixing Vue Template Type Errors",
"description": "A developer encounters 'undefined component' errors in their Vue templates. This skill provides the tsconfig configuration to enable strict template checking and fix these issues.",
"target_user": "Vue developers working with TypeScript and vue-tsc"
},
{
"title": "Optimizing Large Project Save Times",
"description": "A developer experiences 30-60 second save delays in their Vue project. This skill explains how to configure VSCode code actions settings to fix slow save performance.",
"target_user": "Vue developers using VSCode with large codebases"
},
{
"title": "Type-Safe Component Props",
"description": "A developer needs to extract prop types from a Vue component for use in wrapper components. This skill provides TypeScript helper patterns and vue-component-type-helpers usage.",
"target_user": "Vue developers building component libraries or HOCs"
}
],
"prompt_templates": [
{
"title": "Vue Template Type Checking",
"prompt": "How do I enable strict template checking in vue-tsc to catch undefined components? My tsconfig is not catching component name typos.",
"scenario": "Developer wants to enable vue-tsc strict template checking"
},
{
"title": "Fixing Slow Save Times",
"prompt": "My Vue project takes 30+ seconds to save files in VSCode. How do I fix slow save performance caused by code actions?",
"scenario": "Developer experiencing slow file saves in VSCode"
},
{
"title": "Wrapper Component Prop Types",
"prompt": "How do I extract prop types from a Vue component to use in a wrapper component? I need to type my Higher-Order Component correctly.",
"scenario": "Developer building wrapper components needs TypeScript help"
},
{
"title": "Vue Router Params Typing",
"prompt": "How do I properly type Vue Router route params with typed-router? My route params are not type-safe.",
"scenario": "Developer wants type-safe route parameters"
}
],
"output_examples": [
{
"input": "How do I enable strict template checking in Vue?",
"output": [
"To enable strict template checking in vue-tsc, add vueCompilerOptions to your tsconfig.app.json:",
"",
"```json",
"{",
" \"compilerOptions\": {",
" \"strict\": true",
" \"skipLibCheck\": true",
" },",
" \"vueCompilerOptions\": {",
" \"strictTemplates\": true,",
" \"checkUnknownComponents\": true",
" }",
"}",
"```",
"",
"This will catch:",
"- Undefined components used in templates",
"- Props not declared in defineProps",
"- Unknown events emitted by components",
"- Unregistered custom directives"
]
},
{
"input": "Vue file saves are very slow",
"output": [
"Save delays in Vue projects are often caused by VSCode code actions triggering TypeScript synchronization. Fix this by:",
"",
"1. Disable code actions in .vscode/settings.json:",
" ```json",
" { \"vue.codeActions.enabled\": false }",
" ```",
"",
"2. Or limit code action timeout:",
" ```json",
" { \"vue.codeActions.savingTimeLimit\": 1000 }",
" ```",
"",
"3. Upgrade to VSCode 1.81.0+ which includes performance fixes",
"",
"This reduces save time from 30-60 seconds to under 2 seconds."
]
}
],
"best_practices": [
"Enable strictTemplates in vueCompilerOptions to catch template errors at compile time rather than runtime",
"Use vue-component-type-helpers package for type-safe wrapper components and HOCs",
"Configure tsconfig.app.json (not root tsconfig) for Vue source files in create-vue projects"
],
"anti_patterns": [
"Adding vueCompilerOptions to the root tsconfig.json instead of the app-specific tsconfig",
"Not upgrading to VSCode 1.81.0+ when experiencing slow save times",
"Using loose template checking in production codebases where type safety matters"
],
"faq": [
{
"question": "What is the difference between vue-tsc and tsc?",
"answer": "vue-tsc is a wrapper around tsc that adds Vue template type checking. It validates Vue SFC templates using the same TypeScript compiler, catching template errors before runtime."
},
{
"question": "Which tsconfig should I add vueCompilerOptions to?",
"answer": "Add vueCompilerOptions to the tsconfig that includes your Vue source files. In create-vue projects, this is tsconfig.app.json, not the root tsconfig.json or tsconfig.node.json."
},
{
"question": "Why are my Vue file saves so slow?",
"answer": "Save delays are caused by VSCode code actions triggering TypeScript state synchronization. Disable code actions or limit their timeout in VSCode settings to fix this."
},
{
"question": "How do I extract prop types from a Vue component?",
"answer": "Use the ComponentProps type helper from vue-component-type-helpers. Import it as: import type { ComponentProps } from 'vue-component-type-helpers'. Then use ComponentProps<typeof YourComponent>."
},
{
"question": "What does strictTemplates check?",
"answer": "strictTemplates enables multiple checks: unknown components, unknown props, unknown events, and unknown directives. Each can be enabled individually if strictTemplates is too strict."
},
{
"question": "How do I fix Vue plugin duplicate registration in Vite?",
"answer": "If using Vite JavaScript API, ensure the Vue plugin is not loaded in both vite.config.js and inlineConfig. Use configFile: false when providing plugins inline."
}
]
},
"file_structure": [
{
"name": "rules",
"type": "dir",
"path": "rules",
"children": [
{
"name": "codeactions-save-performance.md",
"type": "file",
"path": "rules/codeactions-save-performance.md",
"lines": 80
},
{
"name": "data-attributes-config.md",
"type": "file",
"path": "rules/data-attributes-config.md",
"lines": 75
},
{
"name": "deep-watch-numeric.md",
"type": "file",
"path": "rules/deep-watch-numeric.md",
"lines": 98
},
{
"name": "define-model-update-event.md",
"type": "file",
"path": "rules/define-model-update-event.md",
"lines": 80
},
{
"name": "duplicate-plugin-detection.md",
"type": "file",
"path": "rules/duplicate-plugin-detection.md",
"lines": 103
},
{
"name": "extract-component-props.md",
"type": "file",
"path": "rules/extract-component-props.md",
"lines": 58
},
{
"name": "fallthrough-attributes.md",
"type": "file",
"path": "rules/fallthrough-attributes.md",
"lines": 64
},
{
"name": "hmr-vue-ssr.md",
"type": "file",
"path": "rules/hmr-vue-ssr.md",
"lines": 125
},
{
"name": "module-resolution-bundler.md",
"type": "file",
"path": "rules/module-resolution-bundler.md",
"lines": 82
},
{
"name": "pinia-store-mocking.md",
"type": "file",
"path": "rules/pinia-store-mocking.md",
"lines": 160
},
{
"name": "script-setup-jsdoc.md",
"type": "file",
"path": "rules/script-setup-jsdoc.md",
"lines": 86
},
{
"name": "strict-css-modules.md",
"type": "file",
"path": "rules/strict-css-modules.md",
"lines": 69
},
{
"name": "volar-3-breaking-changes.md",
"type": "file",
"path": "rules/volar-3-breaking-changes.md",
"lines": 66
},
{
"name": "vue-directive-comments.md",
"type": "file",
"path": "rules/vue-directive-comments.md",
"lines": 74
},
{
"name": "vue-router-typed-params.md",
"type": "file",
"path": "rules/vue-router-typed-params.md",
"lines": 82
},
{
"name": "vue-tsc-strict-templates.md",
"type": "file",
"path": "rules/vue-tsc-strict-templates.md",
"lines": 70
},
{
"name": "with-defaults-union-types.md",
"type": "file",
"path": "rules/with-defaults-union-types.md",
"lines": 103
}
]
},
{
"name": "LICENSE.md",
"type": "file",
"path": "LICENSE.md",
"lines": 22
},
{
"name": "SKILL.md",
"type": "file",
"path": "SKILL.md",
"lines": 39
},
{
"name": "SYNC.md",
"type": "file",
"path": "SYNC.md",
"lines": 6
}
]
}
Sync Info
- Source:
vendor/vue-best-practices/skills/vue-best-practices - Git SHA:
ee0ceda40b7fabeb0713caf8b4db5ea438069fb5 - Synced: 2026-01-28
Related skills
FAQ
What Vue version does this target?
Vue 3 with TypeScript, vue-tsc, and Volar.
Does it cover Pinia?
Yes, it includes an efficiency rule for mocking Pinia stores with Vitest.