hookehuyr

docs(vue-best-practices): 新增Vue 3最佳实践文档集

添加关于Vue 3 TypeScript开发、Volar工具链、性能优化和测试相关的13个最佳实践文档,涵盖模板类型检查、CSS模块、路由参数类型等核心场景

- 新增vue-tsc严格模板检查规则
- 添加CSS模块严格类型验证指南
- 补充Volar 3.0迁移问题解决方案
- 包含Pinia测试和SSR HMR调试实践
- 添加defineModel和withDefaults的TypeScript处理方案
- 补充模块解析和自动导入冲突解决建议
1 +---
2 +name: vue-best-practices
3 +description: Vue 3 TypeScript, vue-tsc, Volar, Vite, component props, testing, composition API.
4 +---
5 +
6 +# Vue Best Practices
7 +
8 +## Capability Rules
9 +
10 +| Rule | Keywords | Description |
11 +|------|----------|-------------|
12 +| [vue-tsc-strict-templates](rules/vue-tsc-strict-templates.md) | undefined component, template error, strictTemplates | Catch undefined components in templates |
13 +| [fallthrough-attributes](rules/fallthrough-attributes.md) | fallthrough, $attrs, wrapper component | Type-check fallthrough attributes |
14 +| [strict-css-modules](rules/strict-css-modules.md) | css modules, $style, typo | Catch CSS module class typos |
15 +| [data-attributes-config](rules/data-attributes-config.md) | data-*, strictTemplates, attribute | Allow data-* attributes |
16 +| [volar-3-breaking-changes](rules/volar-3-breaking-changes.md) | volar, vue-language-server, editor | Fix Volar 3.0 upgrade issues |
17 +| [module-resolution-bundler](rules/module-resolution-bundler.md) | cannot find module, @vue/tsconfig, moduleResolution | Fix module resolution errors |
18 +| [unplugin-auto-import-conflicts](rules/unplugin-auto-import-conflicts.md) | unplugin, auto-import, types any | Fix unplugin type conflicts |
19 +| [codeactions-save-performance](rules/codeactions-save-performance.md) | slow save, vscode, performance | Fix slow save in large projects |
20 +| [duplicate-plugin-detection](rules/duplicate-plugin-detection.md) | duplicate plugin, vite, vue plugin | Detect duplicate plugins |
21 +| [define-model-update-event](rules/define-model-update-event.md) | defineModel, update event, undefined | Fix model update errors |
22 +| [with-defaults-union-types](rules/with-defaults-union-types.md) | withDefaults, union type, default | Fix union type defaults |
23 +| [deep-watch-numeric](rules/deep-watch-numeric.md) | watch, deep, array, Vue 3.5 | Efficient array watching |
24 +| [vue-directive-comments](rules/vue-directive-comments.md) | @vue-ignore, @vue-skip, template | Control template type checking |
25 +| [script-setup-jsdoc](rules/script-setup-jsdoc.md) | jsdoc, script setup, documentation | Add JSDoc to script setup |
26 +| [vue-router-typed-params](rules/vue-router-typed-params.md) | route params, typed router, unplugin | Fix route params typing |
27 +
28 +## Efficiency Rules
29 +
30 +| Rule | Keywords | Description |
31 +|------|----------|-------------|
32 +| [hmr-vue-ssr](rules/hmr-vue-ssr.md) | hmr, ssr, hot reload | Fix HMR in SSR apps |
33 +| [pinia-store-mocking](rules/pinia-store-mocking.md) | pinia, mock, vitest, store | Mock Pinia stores |
34 +
35 +## Reference
36 +
37 +- [Vue Language Tools](https://github.com/vuejs/language-tools)
38 +- [Vue 3 Documentation](https://vuejs.org/)
...\ No newline at end of file ...\ No newline at end of file
1 +---
2 +title: Fix Slow Save Times with Code Actions Setting
3 +impact: HIGH
4 +impactDescription: fixes 30-60 second save delays in large Vue projects
5 +type: capability
6 +tags: performance, save-time, vscode, code-actions, volar
7 +---
8 +
9 +# Fix Slow Save Times with Code Actions Setting
10 +
11 +**Impact: HIGH** - fixes 30-60 second save delays in large Vue projects
12 +
13 +In large Vue projects, saving files can take 30-60+ seconds due to VSCode's code actions triggering expensive TypeScript state synchronization.
14 +
15 +## Problem
16 +
17 +Symptoms:
18 +- Save operation takes 30+ seconds
19 +- Editor becomes unresponsive during save
20 +- CPU spikes when saving Vue files
21 +- Happens more in larger projects
22 +
23 +## Root Cause
24 +
25 +VSCode emits document change events multiple times during save cycles. Each event triggers Volar to synchronize with TypeScript, causing expensive re-computation.
26 +
27 +## Solution
28 +
29 +Disable code actions or limit their timeout:
30 +
31 +**Option 1: Disable code actions (fastest)**
32 +```json
33 +// .vscode/settings.json
34 +{
35 + "vue.codeActions.enabled": false
36 +}
37 +```
38 +
39 +**Option 2: Limit code action time**
40 +```json
41 +// .vscode/settings.json
42 +{
43 + "vue.codeActions.savingTimeLimit": 1000
44 +}
45 +```
46 +
47 +**Option 3: Disable specific code actions**
48 +```json
49 +// .vscode/settings.json
50 +{
51 + "vue.codeActions.enabled": true,
52 + "editor.codeActionsOnSave": {
53 + "source.organizeImports": "never"
54 + }
55 +}
56 +```
57 +
58 +## VSCode Version Requirement
59 +
60 +VSCode 1.81.0+ includes fixes that reduce save time issues. Upgrade if using an older version.
61 +
62 +## Additional Optimizations
63 +
64 +```json
65 +// .vscode/settings.json
66 +{
67 + "vue.codeActions.enabled": false,
68 + "editor.formatOnSave": true,
69 + "editor.codeActionsOnSave": {},
70 + "[vue]": {
71 + "editor.formatOnSave": true,
72 + "editor.defaultFormatter": "Vue.volar"
73 + }
74 +}
75 +```
76 +
77 +## Reference
78 +
79 +- [Vue Language Tools Discussion #2740](https://github.com/vuejs/language-tools/discussions/2740)
1 +---
2 +title: Allow Data Attributes with Strict Templates
3 +impact: MEDIUM
4 +impactDescription: fixes data-testid and data-* attribute errors in strict mode
5 +type: capability
6 +tags: dataAttributes, vueCompilerOptions, strictTemplates, data-testid, testing
7 +---
8 +
9 +# Allow Data Attributes with Strict Templates
10 +
11 +**Impact: MEDIUM** - fixes data-testid and data-* attribute errors in strict mode
12 +
13 +With `strictTemplates` enabled, `data-*` attributes on components cause type errors. Use the `dataAttributes` option to allow specific patterns.
14 +
15 +## Problem
16 +
17 +```vue
18 +<template>
19 + <!-- Error: Property 'data-testid' does not exist on type... -->
20 + <MyComponent data-testid="submit-button" />
21 +
22 + <!-- Error: Property 'data-cy' does not exist on type... -->
23 + <MyComponent data-cy="login-form" />
24 +</template>
25 +```
26 +
27 +## Solution
28 +
29 +Configure `dataAttributes` to allow specific patterns:
30 +
31 +```json
32 +// tsconfig.json or tsconfig.app.json
33 +{
34 + "vueCompilerOptions": {
35 + "strictTemplates": true,
36 + "dataAttributes": ["data-*"]
37 + }
38 +}
39 +```
40 +
41 +Now all `data-*` attributes are allowed on any component.
42 +
43 +## Specific Patterns
44 +
45 +You can be more selective:
46 +
47 +```json
48 +{
49 + "vueCompilerOptions": {
50 + "dataAttributes": [
51 + "data-testid",
52 + "data-cy",
53 + "data-test-*"
54 + ]
55 + }
56 +}
57 +```
58 +
59 +This only allows the specified patterns, not all data attributes.
60 +
61 +## Common Testing Attributes
62 +
63 +For testing libraries, allow their specific attributes:
64 +
65 +| Library | Attribute | Pattern |
66 +|---------|-----------|---------|
67 +| Testing Library | `data-testid` | `"data-testid"` |
68 +| Cypress | `data-cy` | `"data-cy"` |
69 +| Playwright | `data-testid` | `"data-testid"` |
70 +| Generic | All data attributes | `"data-*"` |
71 +
72 +## Reference
73 +
74 +- [Vue Language Tools Wiki - Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options)
1 +---
2 +title: Vue 3.5+ Deep Watch Numeric Depth
3 +impact: MEDIUM
4 +impactDescription: enables efficient array mutation watching with numeric deep option
5 +type: capability
6 +tags: watch, deep, vue-3.5, array, mutation, performance
7 +---
8 +
9 +# Vue 3.5+ Deep Watch Numeric Depth
10 +
11 +**Impact: MEDIUM** - enables efficient array mutation watching with numeric deep option
12 +
13 +Vue 3.5 introduced `deep: number` for watch depth control. This allows watching array mutations without the performance cost of deep traversal.
14 +
15 +## Symptoms
16 +
17 +- Array mutations not triggering watch callback
18 +- Deep watch causing performance issues on large nested objects
19 +- Unaware of new Vue 3.5 feature
20 +
21 +> **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.
22 +
23 +## The Feature
24 +
25 +```typescript
26 +// Vue 3.5+ only
27 +watch(items, (newVal) => {
28 + // Triggered on array mutations (push, pop, splice, etc.)
29 +}, { deep: 1 })
30 +```
31 +
32 +| deep value | Behavior |
33 +|------------|----------|
34 +| `true` | Full recursive traversal (original behavior) |
35 +| `false` | Only reference changes |
36 +| `1` | One level deep - array mutations, not nested objects |
37 +| `2` | Two levels deep |
38 +| `n` | N levels deep |
39 +
40 +## Fix
41 +
42 +**Step 1: Ensure Vue 3.5+**
43 +```bash
44 +npm install vue@^3.5.0
45 +```
46 +
47 +**Step 2: Update @vue/runtime-core types**
48 +```bash
49 +npm install -D @vue/runtime-core@latest
50 +```
51 +
52 +**Step 3: Use numeric depth**
53 +```typescript
54 +import { watch, ref } from 'vue'
55 +
56 +const items = ref([{ id: 1, data: { nested: 'value' } }])
57 +
58 +// Watch array mutations only (push, pop, etc.)
59 +watch(items, (newItems) => {
60 + console.log('Array mutated')
61 +}, { deep: 1 })
62 +
63 +// Won't trigger on: items.value[0].data.nested = 'new'
64 +// Will trigger on: items.value.push(newItem)
65 +```
66 +
67 +## Performance Comparison
68 +
69 +```typescript
70 +const largeNestedData = ref({ /* deeply nested structure */ })
71 +
72 +// SLOW - traverses entire structure
73 +watch(largeNestedData, handler, { deep: true })
74 +
75 +// FAST - only watches top-level changes
76 +watch(largeNestedData, handler, { deep: 1 })
77 +
78 +// FASTEST - only reference changes
79 +watch(largeNestedData, handler, { deep: false })
80 +```
81 +
82 +## Alternative: watchEffect for Selective Tracking
83 +
84 +```typescript
85 +// Only tracks properties actually accessed
86 +watchEffect(() => {
87 + // Only re-runs when items.value.length or first item changes
88 + console.log(items.value.length, items.value[0]?.id)
89 +})
90 +```
91 +
92 +## TypeScript Note
93 +
94 +If TypeScript complains about numeric deep, ensure:
95 +1. Vue version is 3.5+
96 +2. `@vue/runtime-core` types are updated
97 +3. tsconfig targets correct node_modules types
98 +
99 +## Reference
100 +
101 +- [Vue Watchers Docs](https://vuejs.org/guide/essentials/watchers.html)
102 +- [Vue 3.5 Release Notes](https://blog.vuejs.org/posts/vue-3-5)
1 +---
2 +title: defineModel Fires Update Event with Undefined
3 +impact: MEDIUM
4 +impactDescription: fixes runtime errors from unexpected undefined in model updates
5 +type: capability
6 +tags: defineModel, v-model, update-event, undefined, vue-3.5
7 +---
8 +
9 +# defineModel Fires Update Event with Undefined
10 +
11 +**Impact: MEDIUM** - fixes runtime errors from unexpected undefined in model updates
12 +
13 +> **Version Note (2025):** 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.
14 +
15 +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.
16 +
17 +## Symptoms
18 +
19 +- Parent component receives `undefined` unexpectedly
20 +- Runtime error: "Cannot read property of undefined"
21 +- Type mismatch between expected `T` and received `T | undefined`
22 +- Issue appears when clearing/resetting the model value
23 +
24 +## Root Cause
25 +
26 +`defineModel` returns `Ref<T | undefined>` by default, even when `T` is non-nullable. The update event can fire with `undefined` when:
27 +- Component unmounts
28 +- Model is explicitly cleared
29 +- Internal state resets
30 +
31 +## Fix
32 +
33 +**Option 1: Use required option (Vue 3.5+)**
34 +```typescript
35 +// Returns Ref<Item> instead of Ref<Item | undefined>
36 +const model = defineModel<Item>({ required: true })
37 +```
38 +
39 +**Option 2: Type parent handler to accept undefined**
40 +```vue
41 +<template>
42 + <MyComponent
43 + v-model="item"
44 + @update:model-value="handleUpdate"
45 + />
46 +</template>
47 +
48 +<script setup lang="ts">
49 +// Handle both value and undefined
50 +const handleUpdate = (value: Item | undefined) => {
51 + if (value !== undefined) {
52 + item.value = value
53 + }
54 +}
55 +</script>
56 +```
57 +
58 +**Option 3: Use default value in defineModel**
59 +```typescript
60 +const model = defineModel<string>({ default: '' })
61 +```
62 +
63 +## Type Declaration Pattern
64 +
65 +```typescript
66 +// In child component
67 +interface Props {
68 + modelValue: Item
69 +}
70 +const model = defineModel<Item>({ required: true })
71 +
72 +// Emits will be typed as (value: Item) not (value: Item | undefined)
73 +```
74 +
75 +## Reference
76 +
77 +- [vuejs/core#12817](https://github.com/vuejs/core/issues/12817)
78 +- [vuejs/core#10103](https://github.com/vuejs/core/issues/10103)
79 +- [defineModel docs](https://vuejs.org/api/sfc-script-setup.html#definemodel)
1 +---
2 +title: Duplicate Vue Plugin Detection
3 +impact: MEDIUM
4 +impactDescription: fixes cryptic build errors from Vue plugin registered twice
5 +type: capability
6 +tags: vite, plugin, vue, duplicate, config, inline
7 +---
8 +
9 +# Duplicate Vue Plugin Detection
10 +
11 +**Impact: MEDIUM** - fixes cryptic build errors from Vue plugin registered twice
12 +
13 +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.
14 +
15 +## Symptoms
16 +
17 +- Build produces unexpected output or fails silently
18 +- "Cannot read property of undefined" during build
19 +- Different build behavior between CLI and JavaScript API
20 +- Vue components render incorrectly after build
21 +
22 +## Root Cause
23 +
24 +Vite doesn't deduplicate plugins by name when merging configs. The Vue plugin's internal state gets corrupted when registered twice.
25 +
26 +## Fix
27 +
28 +**Option 1: Use configFile: false with inline plugins**
29 +```typescript
30 +import { build } from 'vite'
31 +import vue from '@vitejs/plugin-vue'
32 +
33 +await build({
34 + configFile: false, // Don't load vite.config.js
35 + plugins: [vue()],
36 + // ... rest of config
37 +})
38 +```
39 +
40 +**Option 2: Don't specify plugins in inlineConfig**
41 +```typescript
42 +// vite.config.js already has vue plugin
43 +import { build } from 'vite'
44 +
45 +await build({
46 + // Don't add vue plugin here - it's in vite.config.js
47 + root: './src',
48 + build: { outDir: '../dist' }
49 +})
50 +```
51 +
52 +**Option 3: Filter out Vue plugin before merging**
53 +```typescript
54 +import { build, loadConfigFromFile } from 'vite'
55 +import vue from '@vitejs/plugin-vue'
56 +
57 +const { config } = await loadConfigFromFile({ command: 'build', mode: 'production' })
58 +
59 +// Remove existing Vue plugin
60 +const filteredPlugins = config.plugins?.filter(
61 + p => !p || (Array.isArray(p) ? false : p.name !== 'vite:vue')
62 +) || []
63 +
64 +await build({
65 + ...config,
66 + plugins: [...filteredPlugins, vue({ /* your options */ })]
67 +})
68 +```
69 +
70 +## Detection Script
71 +
72 +Add this to debug plugin registration:
73 +```typescript
74 +// vite.config.ts
75 +export default defineConfig({
76 + plugins: [
77 + vue(),
78 + {
79 + name: 'debug-plugins',
80 + configResolved(config) {
81 + const vuePlugins = config.plugins.filter(p => p.name?.includes('vue'))
82 + if (vuePlugins.length > 1) {
83 + console.warn('WARNING: Multiple Vue plugins detected:', vuePlugins.map(p => p.name))
84 + }
85 + }
86 + }
87 + ]
88 +})
89 +```
90 +
91 +## Common Scenarios
92 +
93 +| Scenario | Solution |
94 +|----------|----------|
95 +| Using `vite.createServer()` | Use `configFile: false` |
96 +| Build script with custom config | Don't duplicate plugins |
97 +| Monorepo with shared config | Check for plugin inheritance |
98 +
99 +## Reference
100 +
101 +- [Vite Issue #5335](https://github.com/vitejs/vite/issues/5335)
102 +- [Vite JavaScript API](https://vite.dev/guide/api-javascript.html)
1 +---
2 +title: Enable Fallthrough Attributes Type Checking
3 +impact: HIGH
4 +impactDescription: enables type-safe fallthrough attributes in component libraries
5 +type: capability
6 +tags: fallthroughAttributes, vueCompilerOptions, component-library, wrapper-components
7 +---
8 +
9 +# Enable Fallthrough Attributes Type Checking
10 +
11 +**Impact: MEDIUM** - enables type-aware attribute forwarding in component libraries
12 +
13 +When building component libraries with wrapper components, enable `fallthroughAttributes` to get IDE autocomplete for attributes that will be forwarded to child elements.
14 +
15 +## What It Does
16 +
17 +Wrapper components that pass attributes to child elements can benefit from type-aware completion:
18 +
19 +```vue
20 +<!-- MyButton.vue - wrapper around native button -->
21 +<template>
22 + <button v-bind="$attrs"><slot /></button>
23 +</template>
24 +```
25 +
26 +## Solution
27 +
28 +Enable `fallthroughAttributes` in your tsconfig:
29 +
30 +```json
31 +// tsconfig.json or tsconfig.app.json
32 +{
33 + "vueCompilerOptions": {
34 + "fallthroughAttributes": true
35 + }
36 +}
37 +```
38 +
39 +## How It Works
40 +
41 +When `fallthroughAttributes: true`:
42 +- Vue Language Server analyzes which element receives `$attrs`
43 +- IDE autocomplete suggests valid attributes for the target element
44 +- Helps developers discover available attributes
45 +
46 +> **Note:** This primarily enables IDE autocomplete for valid fallthrough attributes. It does NOT reject invalid attributes as type errors - arbitrary attributes are still allowed.
47 +
48 +## Related Options
49 +
50 +Combine with `strictTemplates` for comprehensive checking:
51 +
52 +```json
53 +{
54 + "vueCompilerOptions": {
55 + "strictTemplates": true,
56 + "fallthroughAttributes": true
57 + }
58 +}
59 +```
60 +
61 +## Reference
62 +
63 +- [Vue Language Tools Wiki - Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options)
1 +---
2 +title: HMR Debugging for Vue SSR
3 +impact: MEDIUM
4 +impactDescription: fixes Hot Module Replacement breaking in Vue SSR applications
5 +type: efficiency
6 +tags: vite, hmr, ssr, vue, hot-reload, server-side-rendering
7 +---
8 +
9 +# HMR Debugging for Vue SSR
10 +
11 +**Impact: MEDIUM** - fixes Hot Module Replacement breaking in Vue SSR applications
12 +
13 +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.
14 +
15 +## Symptoms
16 +
17 +- HMR works for `<template>` changes but breaks for `<script setup>`
18 +- "Cannot read property of undefined" after saving
19 +- Full page reload required after script changes
20 +- HMR works in dev:client but not dev:ssr
21 +
22 +## Root Cause
23 +
24 +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.
25 +
26 +## Fix
27 +
28 +**Step 1: Ensure correct SSR plugin configuration**
29 +```typescript
30 +// vite.config.ts
31 +import { defineConfig } from 'vite'
32 +import vue from '@vitejs/plugin-vue'
33 +
34 +export default defineConfig({
35 + plugins: [vue()],
36 + ssr: {
37 + // Don't externalize these for HMR to work
38 + noExternal: ['vue', '@vue/runtime-core', '@vue/runtime-dom']
39 + }
40 +})
41 +```
42 +
43 +**Step 2: Configure dev server for SSR HMR**
44 +```typescript
45 +// server.ts
46 +import { createServer } from 'vite'
47 +
48 +const vite = await createServer({
49 + server: { middlewareMode: true },
50 + appType: 'custom'
51 +})
52 +
53 +// Use vite.ssrLoadModule for server-side imports
54 +const { render } = await vite.ssrLoadModule('/src/entry-server.ts')
55 +
56 +// Handle HMR
57 +vite.watcher.on('change', async (file) => {
58 + if (file.endsWith('.vue')) {
59 + // Invalidate the module
60 + const mod = vite.moduleGraph.getModuleById(file)
61 + if (mod) {
62 + vite.moduleGraph.invalidateModule(mod)
63 + }
64 + }
65 +})
66 +```
67 +
68 +**Step 3: Add HMR acceptance in entry-server**
69 +```typescript
70 +// entry-server.ts
71 +import { createApp } from './main'
72 +
73 +export async function render(url: string) {
74 + const app = createApp()
75 + // ... render logic
76 +}
77 +
78 +// Accept HMR updates
79 +if (import.meta.hot) {
80 + import.meta.hot.accept()
81 +}
82 +```
83 +
84 +## Framework-Specific Solutions
85 +
86 +### Nuxt 3
87 +HMR should work out of the box. If not:
88 +```bash
89 +rm -rf .nuxt node_modules/.vite
90 +npm install
91 +npm run dev
92 +```
93 +
94 +### Vite SSR Template
95 +Ensure you're using the latest `@vitejs/plugin-vue`:
96 +```bash
97 +npm install @vitejs/plugin-vue@latest
98 +```
99 +
100 +## Debugging
101 +
102 +Enable verbose HMR logging:
103 +```typescript
104 +// vite.config.ts
105 +export default defineConfig({
106 + server: {
107 + hmr: {
108 + overlay: true
109 + }
110 + },
111 + logLevel: 'info' // Shows HMR updates
112 +})
113 +```
114 +
115 +## Known Limitations
116 +
117 +- HMR for `<script>` (not `<script setup>`) may require full reload
118 +- SSR components with external dependencies may not hot-reload
119 +- State is not preserved for SSR components (expected behavior)
120 +
121 +## Reference
122 +
123 +- [vite-plugin-vue#525](https://github.com/vitejs/vite-plugin-vue/issues/525)
124 +- [Vite SSR Guide](https://vite.dev/guide/ssr.html)
1 +---
2 +title: moduleResolution Bundler Migration Issues
3 +impact: HIGH
4 +impactDescription: fixes "Cannot find module" errors after @vue/tsconfig upgrade
5 +type: capability
6 +tags: moduleResolution, bundler, tsconfig, vue-tsconfig, node, esm
7 +---
8 +
9 +# moduleResolution Bundler Migration Issues
10 +
11 +**Impact: HIGH** - fixes "Cannot find module" errors after @vue/tsconfig upgrade
12 +
13 +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`.
14 +
15 +## Symptoms
16 +
17 +- `Cannot find module 'vue'` or other packages
18 +- `Option '--resolveJsonModule' cannot be specified without 'node' module resolution`
19 +- Errors appear after updating `@vue/tsconfig`
20 +- Some third-party packages no longer resolve
21 +
22 +## Root Cause
23 +
24 +`moduleResolution: "bundler"` requires:
25 +1. TypeScript 5.0+
26 +2. Packages to have proper `exports` field in package.json
27 +3. Different resolution rules than Node.js classic resolution
28 +
29 +## Fix
30 +
31 +**Option 1: Ensure TypeScript 5.0+ everywhere**
32 +```bash
33 +npm install -D typescript@^5.0.0
34 +```
35 +
36 +In monorepos, ALL packages must use TypeScript 5.0+.
37 +
38 +**Option 2: Add compatibility workaround**
39 +```json
40 +{
41 + "compilerOptions": {
42 + "module": "ESNext",
43 + "moduleResolution": "bundler",
44 + "resolvePackageJsonExports": false
45 + }
46 +}
47 +```
48 +
49 +Setting `resolvePackageJsonExports: false` restores compatibility with packages that don't have proper exports.
50 +
51 +**Option 3: Revert to Node resolution**
52 +```json
53 +{
54 + "compilerOptions": {
55 + "moduleResolution": "node"
56 + }
57 +}
58 +```
59 +
60 +## Which Packages Break?
61 +
62 +Packages break if they:
63 +- Lack `exports` field in package.json
64 +- Have incorrect `exports` configuration
65 +- Rely on Node.js-specific resolution behavior
66 +
67 +## Diagnosis
68 +
69 +```bash
70 +# Check which resolution is being used
71 +cat tsconfig.json | grep moduleResolution
72 +
73 +# Test if a specific module resolves
74 +npx tsc --traceResolution 2>&1 | grep "module-name"
75 +```
76 +
77 +## Reference
78 +
79 +- [vuejs/tsconfig#8](https://github.com/vuejs/tsconfig/issues/8)
80 +- [TypeScript moduleResolution docs](https://www.typescriptlang.org/tsconfig#moduleResolution)
81 +- [Vite discussion#14001](https://github.com/vitejs/vite/discussions/14001)
1 +---
2 +title: Mocking Pinia Stores with Vitest
3 +impact: HIGH
4 +impactDescription: properly mocks Pinia stores in component tests
5 +type: efficiency
6 +tags: pinia, vitest, testing, mock, createTestingPinia, store
7 +---
8 +
9 +# Mocking Pinia Stores with Vitest
10 +
11 +**Impact: HIGH** - properly mocks Pinia stores in component tests
12 +
13 +Developers struggle to properly mock Pinia stores: `createTestingPinia` requires explicit `createSpy` configuration, and "injection Symbol(pinia) not found" errors occur without proper setup.
14 +
15 +> **Important (@pinia/testing 1.0+):** The `createSpy` option is **REQUIRED**, not optional. Omitting it throws an error: "You must configure the `createSpy` option."
16 +
17 +## Symptoms
18 +
19 +- "injection Symbol(pinia) not found" error
20 +- "You must configure the `createSpy` option" error
21 +- Actions not properly mocked
22 +- Store state not reset between tests
23 +
24 +## Fix
25 +
26 +**Pattern 1: Basic setup with createTestingPinia**
27 +```typescript
28 +import { mount } from '@vue/test-utils'
29 +import { createTestingPinia } from '@pinia/testing'
30 +import { vi } from 'vitest'
31 +import MyComponent from './MyComponent.vue'
32 +import { useCounterStore } from '@/stores/counter'
33 +
34 +test('component uses store', async () => {
35 + const wrapper = mount(MyComponent, {
36 + global: {
37 + plugins: [
38 + createTestingPinia({
39 + createSpy: vi.fn, // REQUIRED in @pinia/testing 1.0+
40 + initialState: {
41 + counter: { count: 10 } // Set initial state
42 + }
43 + })
44 + ]
45 + }
46 + })
47 +
48 + // Get the store instance AFTER mounting
49 + const store = useCounterStore()
50 +
51 + // Actions are automatically stubbed
52 + await wrapper.find('button').trigger('click')
53 + expect(store.increment).toHaveBeenCalled()
54 +})
55 +```
56 +
57 +**Pattern 2: Customize action behavior**
58 +```typescript
59 +test('component handles async action', async () => {
60 + const wrapper = mount(MyComponent, {
61 + global: {
62 + plugins: [
63 + createTestingPinia({
64 + createSpy: vi.fn,
65 + stubActions: false // Don't stub, use real actions
66 + })
67 + ]
68 + }
69 + })
70 +
71 + const store = useCounterStore()
72 +
73 + // Override specific action
74 + store.fetchData = vi.fn().mockResolvedValue({ items: [] })
75 +
76 + await wrapper.find('.load-button').trigger('click')
77 + expect(store.fetchData).toHaveBeenCalled()
78 +})
79 +```
80 +
81 +**Pattern 3: Testing store directly**
82 +```typescript
83 +import { setActivePinia, createPinia } from 'pinia'
84 +import { useCounterStore } from '@/stores/counter'
85 +
86 +describe('Counter Store', () => {
87 + beforeEach(() => {
88 + setActivePinia(createPinia())
89 + })
90 +
91 + test('increments count', () => {
92 + const store = useCounterStore()
93 + expect(store.count).toBe(0)
94 +
95 + store.increment()
96 + expect(store.count).toBe(1)
97 + })
98 +})
99 +```
100 +
101 +## Setup Store with Vitest
102 +
103 +```typescript
104 +// stores/counter.ts - Setup store syntax
105 +export const useCounterStore = defineStore('counter', () => {
106 + const count = ref(0)
107 + const doubleCount = computed(() => count.value * 2)
108 +
109 + function increment() {
110 + count.value++
111 + }
112 +
113 + return { count, doubleCount, increment }
114 +})
115 +
116 +// Test file
117 +test('setup store works', async () => {
118 + const pinia = createTestingPinia({
119 + createSpy: vi.fn,
120 + initialState: {
121 + counter: { count: 5 }
122 + }
123 + })
124 +
125 + const wrapper = mount(MyComponent, {
126 + global: { plugins: [pinia] }
127 + })
128 +
129 + const store = useCounterStore()
130 + expect(store.count).toBe(5)
131 + expect(store.doubleCount).toBe(10)
132 +})
133 +```
134 +
135 +## Reset Between Tests
136 +
137 +```typescript
138 +describe('Store Tests', () => {
139 + let pinia: Pinia
140 +
141 + beforeEach(() => {
142 + pinia = createTestingPinia({
143 + createSpy: vi.fn
144 + })
145 + })
146 +
147 + afterEach(() => {
148 + vi.clearAllMocks()
149 + })
150 +
151 + test('test 1', () => { /* ... */ })
152 + test('test 2', () => { /* ... */ })
153 +})
154 +```
155 +
156 +## Reference
157 +
158 +- [Pinia Testing Guide](https://pinia.vuejs.org/cookbook/testing.html)
159 +- [Pinia Discussion #2092](https://github.com/vuejs/pinia/discussions/2092)
1 +---
2 +title: JSDoc Documentation for Script Setup Components
3 +impact: MEDIUM
4 +impactDescription: enables proper documentation for composition API components
5 +type: capability
6 +tags: jsdoc, script-setup, documentation, composition-api, component
7 +---
8 +
9 +# JSDoc Documentation for Script Setup Components
10 +
11 +**Impact: MEDIUM** - enables proper documentation for composition API components
12 +
13 +`<script setup>` doesn't have an obvious place to attach JSDoc comments for the component itself. Use a dual-script pattern.
14 +
15 +## Problem
16 +
17 +**Incorrect:**
18 +```vue
19 +<script setup lang="ts">
20 +/**
21 + * This comment doesn't appear in IDE hover or docs
22 + * @component
23 + */
24 +import { ref } from 'vue'
25 +
26 +const count = ref(0)
27 +</script>
28 +```
29 +
30 +JSDoc comments inside `<script setup>` don't attach to the component export because there's no explicit export statement.
31 +
32 +## Solution
33 +
34 +Use both `<script>` and `<script setup>` blocks:
35 +
36 +**Correct:**
37 +```vue
38 +<script lang="ts">
39 +/**
40 + * A counter component that displays and increments a value.
41 + *
42 + * @example
43 + * ```vue
44 + * <Counter :initial="5" @update="handleUpdate" />
45 + * ```
46 + *
47 + * @component
48 + */
49 +export default {}
50 +</script>
51 +
52 +<script setup lang="ts">
53 +import { ref } from 'vue'
54 +
55 +const props = defineProps<{
56 + /** Starting value for the counter */
57 + initial?: number
58 +}>()
59 +
60 +const emit = defineEmits<{
61 + /** Emitted when counter value changes */
62 + update: [value: number]
63 +}>()
64 +
65 +const count = ref(props.initial ?? 0)
66 +</script>
67 +```
68 +
69 +## How It Works
70 +
71 +- The regular `<script>` block's default export is merged with `<script setup>`
72 +- JSDoc on `export default {}` attaches to the component
73 +- Props and emits JSDoc in `<script setup>` still work normally
74 +
75 +## What Gets Documented
76 +
77 +| Location | Shows In |
78 +|----------|----------|
79 +| `export default {}` JSDoc | Component import hover |
80 +| `defineProps` JSDoc | Prop hover in templates |
81 +| `defineEmits` JSDoc | Event handler hover |
82 +
83 +## Reference
84 +
85 +- [Vue Language Tools Discussion #5932](https://github.com/vuejs/language-tools/discussions/5932)
1 +---
2 +title: Enable Strict CSS Modules Type Checking
3 +impact: MEDIUM
4 +impactDescription: catches typos in CSS module class names at compile time
5 +type: capability
6 +tags: strictCssModules, vueCompilerOptions, css-modules, style-module
7 +---
8 +
9 +# Enable Strict CSS Modules Type Checking
10 +
11 +**Impact: MEDIUM** - catches typos in CSS module class names at compile time
12 +
13 +When using CSS modules with `<style module>`, Vue doesn't validate class names by default. Enable `strictCssModules` to catch typos and undefined classes.
14 +
15 +## Problem
16 +
17 +CSS module class name errors go undetected:
18 +
19 +```vue
20 +<script setup lang="ts">
21 +// No error for typo in class name
22 +</script>
23 +
24 +<template>
25 + <div :class="$style.buttn">Click me</div>
26 +</template>
27 +
28 +<style module>
29 +.button {
30 + background: blue;
31 +}
32 +</style>
33 +```
34 +
35 +The typo `buttn` instead of `button` silently fails at runtime.
36 +
37 +## Solution
38 +
39 +Enable `strictCssModules` in your tsconfig:
40 +
41 +```json
42 +// tsconfig.json or tsconfig.app.json
43 +{
44 + "vueCompilerOptions": {
45 + "strictCssModules": true
46 + }
47 +}
48 +```
49 +
50 +Now `$style.buttn` will show a type error because `buttn` doesn't exist in the CSS module.
51 +
52 +## What Gets Checked
53 +
54 +| Access | With strictCssModules |
55 +|--------|----------------------|
56 +| `$style.validClass` | OK |
57 +| `$style.typo` | Error: Property 'typo' does not exist |
58 +| `$style['dynamic']` | OK (dynamic access not checked) |
59 +
60 +## Limitations
61 +
62 +- Only checks static property access (`$style.className`)
63 +- Dynamic access (`$style[variable]`) is not validated
64 +- Only works with `<style module>`, not external CSS files
65 +
66 +## Reference
67 +
68 +- [Vue Language Tools Wiki - Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options)
1 +---
2 +title: unplugin-vue-components and unplugin-auto-import Type Conflicts
3 +impact: HIGH
4 +impactDescription: fixes component types resolving as any when using both plugins
5 +type: capability
6 +tags: unplugin-vue-components, unplugin-auto-import, types, any, dts
7 +---
8 +
9 +# unplugin-vue-components and unplugin-auto-import Type Conflicts
10 +
11 +**Impact: HIGH** - fixes component types resolving as any when using both plugins
12 +
13 +Installing both `unplugin-vue-components` and `unplugin-auto-import` can cause component types to resolve as `any`. The generated `.d.ts` files conflict with each other.
14 +
15 +## Symptoms
16 +
17 +- Components typed as `any` instead of proper component types
18 +- No autocomplete for component props
19 +- No type errors for invalid props
20 +- Types work when using only one plugin but break with both
21 +
22 +## Root Cause
23 +
24 +Both plugins generate declaration files (`components.d.ts` and `auto-imports.d.ts`) that can have conflicting declarations. TypeScript declaration merging fails silently.
25 +
26 +## Fix
27 +
28 +**Step 1: Ensure both .d.ts files are in tsconfig include**
29 +```json
30 +{
31 + "include": [
32 + "src/**/*.ts",
33 + "src/**/*.vue",
34 + "components.d.ts",
35 + "auto-imports.d.ts"
36 + ]
37 +}
38 +```
39 +
40 +**Step 2: Set explicit, different dts paths**
41 +```typescript
42 +// vite.config.ts
43 +import Components from 'unplugin-vue-components/vite'
44 +import AutoImport from 'unplugin-auto-import/vite'
45 +
46 +export default defineConfig({
47 + plugins: [
48 + Components({
49 + dts: 'src/types/components.d.ts' // Explicit unique path
50 + }),
51 + AutoImport({
52 + dts: 'src/types/auto-imports.d.ts' // Explicit unique path
53 + })
54 + ]
55 +})
56 +```
57 +
58 +**Step 3: Regenerate type files**
59 +```bash
60 +# Delete existing .d.ts files
61 +rm components.d.ts auto-imports.d.ts
62 +
63 +# Restart dev server to regenerate
64 +npm run dev
65 +```
66 +
67 +**Step 4: Verify no duplicate declarations**
68 +
69 +Check that the same component isn't declared in both files.
70 +
71 +## Plugin Order Matters
72 +
73 +Configure Components plugin AFTER AutoImport:
74 +```typescript
75 +plugins: [
76 + AutoImport({ /* ... */ }),
77 + Components({ /* ... */ }) // Must come after AutoImport
78 +]
79 +```
80 +
81 +## Common Mistake: Duplicate Imports
82 +
83 +Don't configure the same import in both plugins:
84 +```typescript
85 +// Wrong - Vue imported in both
86 +AutoImport({
87 + imports: ['vue']
88 +})
89 +Components({
90 + resolvers: [/* includes Vue components */]
91 +})
92 +```
93 +
94 +## Reference
95 +
96 +- [unplugin-vue-components#640](https://github.com/unplugin/unplugin-vue-components/issues/640)
97 +- [unplugin-auto-import docs](https://github.com/unplugin/unplugin-auto-import)
1 +---
2 +title: Volar 3.0 Breaking Changes
3 +impact: HIGH
4 +impactDescription: fixes editor integration after Volar/vue-language-server upgrade
5 +type: capability
6 +tags: volar, vue-language-server, neovim, vscode, ide, ts_ls, vtsls
7 +---
8 +
9 +# Volar 3.0 Breaking Changes
10 +
11 +**Impact: HIGH** - fixes editor integration after Volar/vue-language-server upgrade
12 +
13 +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".
14 +
15 +## Symptoms
16 +
17 +- `vue_ls doesn't work with ts_ls`
18 +- TypeScript features stop working in Vue files
19 +- No autocomplete, type hints, or error highlighting
20 +- Editor shows "Language server initialization failed"
21 +
22 +## Fix by Editor
23 +
24 +### VSCode
25 +
26 +Update the "Vue - Official" extension to latest version. It manages the language server automatically.
27 +
28 +### NeoVim (nvim-lspconfig)
29 +
30 +**Option 1: Use vtsls instead of ts_ls**
31 +```lua
32 +-- Replace ts_ls/tsserver with vtsls
33 +require('lspconfig').vtsls.setup({})
34 +require('lspconfig').volar.setup({})
35 +```
36 +
37 +**Option 2: Downgrade vue-language-server**
38 +```bash
39 +npm install -g @vue/language-server@2.1.10
40 +```
41 +
42 +### JetBrains IDEs
43 +
44 +Update to latest Vue plugin. If issues persist, disable and re-enable the Vue plugin.
45 +
46 +## What Changed in 3.0
47 +
48 +| Feature | Volar 2.x | Volar 3.0 |
49 +|---------|-----------|-----------|
50 +| Package name | volar | vue_ls |
51 +| TypeScript integration | ts_ls/tsserver | vtsls required |
52 +| Hybrid mode | Optional | Default |
53 +
54 +## Workaround: Stay on 2.x
55 +
56 +If upgrading is not possible:
57 +```bash
58 +npm install -g @vue/language-server@^2.0.0
59 +```
60 +
61 +Pin in your project's package.json to prevent accidental upgrades.
62 +
63 +## Reference
64 +
65 +- [vuejs/language-tools#5598](https://github.com/vuejs/language-tools/issues/5598)
66 +- [NeoVim Vue Setup Guide](https://dev.to/danwalsh/solved-vue-3-typescript-inlay-hint-support-in-neovim-53ej)
1 +---
2 +title: Vue Template Directive Comments
3 +impact: HIGH
4 +impactDescription: enables fine-grained control over template type checking
5 +type: capability
6 +tags: vue-directive, vue-ignore, vue-expect-error, vue-skip, template, type-checking
7 +---
8 +
9 +# Vue Template Directive Comments
10 +
11 +**Impact: HIGH** - enables fine-grained control over template type checking
12 +
13 +Vue Language Tools supports special directive comments to control type checking behavior in templates.
14 +
15 +## Available Directives
16 +
17 +### @vue-ignore
18 +
19 +Suppress type errors for the next line:
20 +
21 +```vue
22 +<template>
23 + <!-- @vue-ignore -->
24 + <Component :prop="valueWithTypeError" />
25 +</template>
26 +```
27 +
28 +### @vue-expect-error
29 +
30 +Assert that the next line should have a type error (useful for testing):
31 +
32 +```vue
33 +<template>
34 + <!-- @vue-expect-error -->
35 + <Component :invalid-prop="value" />
36 +</template>
37 +```
38 +
39 +### @vue-skip
40 +
41 +Skip type checking for an entire block:
42 +
43 +```vue
44 +<template>
45 + <!-- @vue-skip -->
46 + <div>
47 + <!-- Everything in here is not type-checked -->
48 + <LegacyComponent :any="props" :go="here" />
49 + </div>
50 +</template>
51 +```
52 +
53 +### @vue-generic
54 +
55 +Declare template-level generic types:
56 +
57 +```vue
58 +<template>
59 + <!-- @vue-generic {T extends string} -->
60 + <GenericList :items="items as T[]" />
61 +</template>
62 +```
63 +
64 +## Use Cases
65 +
66 +- Migrating legacy components with incomplete types
67 +- Working with third-party components that have incorrect type definitions
68 +- Temporarily suppressing errors during refactoring
69 +- Testing that certain patterns produce expected type errors
70 +
71 +## Reference
72 +
73 +- [Vue Language Tools Wiki - Directive Comments](https://github.com/vuejs/language-tools/wiki/Directive-Comments)
1 +---
2 +title: Vue Router useRoute Params Union Type Narrowing
3 +impact: MEDIUM
4 +impactDescription: fixes "Property does not exist" errors with typed route params
5 +type: capability
6 +tags: vue-router, useRoute, unplugin-vue-router, typed-routes, params
7 +---
8 +
9 +# Vue Router useRoute Params Union Type Narrowing
10 +
11 +**Impact: MEDIUM** - fixes "Property does not exist" errors with typed route params
12 +
13 +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.
14 +
15 +## Symptoms
16 +
17 +- "Property 'id' does not exist on type 'RouteParams'"
18 +- `route.params.id` shows as `string | undefined` everywhere
19 +- Union type of all route params instead of specific route
20 +- Type narrowing with `if (route.name === 'users-id')` doesn't work
21 +
22 +## Root Cause
23 +
24 +`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.
25 +
26 +## Fix
27 +
28 +**Option 1: Pass route name to useRoute (recommended)**
29 +```typescript
30 +// pages/users/[id].vue
31 +import { useRoute } from 'vue-router/auto'
32 +
33 +// Specify the route path for proper typing
34 +const route = useRoute('/users/[id]')
35 +
36 +// Now properly typed as { id: string }
37 +console.log(route.params.id) // string, not string | undefined
38 +```
39 +
40 +**Option 2: Type assertion with specific route**
41 +```typescript
42 +import { useRoute } from 'vue-router'
43 +import type { RouteLocationNormalized } from 'vue-router/auto-routes'
44 +
45 +const route = useRoute() as RouteLocationNormalized<'/users/[id]'>
46 +route.params.id // Properly typed
47 +```
48 +
49 +**Option 3: Define route-specific param type**
50 +```typescript
51 +// In your page component
52 +interface UserRouteParams {
53 + id: string
54 +}
55 +
56 +const route = useRoute()
57 +const { id } = route.params as UserRouteParams
58 +```
59 +
60 +## Required tsconfig Setting
61 +
62 +Ensure `moduleResolution: "bundler"` for unplugin-vue-router:
63 +```json
64 +{
65 + "compilerOptions": {
66 + "moduleResolution": "bundler"
67 + }
68 +}
69 +```
70 +
71 +## Caveat: Route Name Format
72 +
73 +The route name matches the file path pattern:
74 +- `pages/users/[id].vue``/users/[id]`
75 +- `pages/posts/[slug]/comments.vue``/posts/[slug]/comments`
76 +
77 +## Reference
78 +
79 +- [unplugin-vue-router#337](https://github.com/posva/unplugin-vue-router/issues/337)
80 +- [unplugin-vue-router#176](https://github.com/posva/unplugin-vue-router/discussions/176)
81 +- [unplugin-vue-router TypeScript docs](https://uvr.esm.is/guide/typescript.html)
1 +---
2 +title: Enable Strict Template Checking
3 +impact: HIGH
4 +impactDescription: catches undefined components and props at compile time
5 +type: capability
6 +tags: vue-tsc, typescript, type-checking, templates, vueCompilerOptions
7 +---
8 +
9 +# Enable Strict Template Checking
10 +
11 +**Impact: HIGH** - catches undefined components and props at compile time
12 +
13 +By default, vue-tsc does not report errors for undefined components in templates. Enable `strictTemplates` to catch these issues during type checking.
14 +
15 +## Which tsconfig?
16 +
17 +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`.
18 +
19 +**Incorrect (missing strict checking):**
20 +
21 +```json
22 +{
23 + "compilerOptions": {
24 + "strict": true
25 + }
26 + // vueCompilerOptions not configured - undefined components won't error
27 +}
28 +```
29 +
30 +**Correct (strict template checking enabled):**
31 +
32 +```json
33 +{
34 + "compilerOptions": {
35 + "strict": true
36 + },
37 + "vueCompilerOptions": {
38 + "strictTemplates": true
39 + }
40 +}
41 +```
42 +
43 +## Available Options
44 +
45 +| Option | Default | Effect |
46 +|--------|---------|--------|
47 +| `strictTemplates` | `false` | Enables all checkUnknown* options below |
48 +| `checkUnknownComponents` | `false` | Error on undefined/unregistered components |
49 +| `checkUnknownProps` | `false` | Error on props not declared in component definition |
50 +| `checkUnknownEvents` | `false` | Error on events not declared via `defineEmits` |
51 +| `checkUnknownDirectives` | `false` | Error on unregistered custom directives |
52 +
53 +## Granular Control
54 +
55 +If `strictTemplates` is too strict, enable individual checks:
56 +
57 +```json
58 +{
59 + "vueCompilerOptions": {
60 + "checkUnknownComponents": true,
61 + "checkUnknownProps": false
62 + }
63 +}
64 +```
65 +
66 +## Reference
67 +
68 +- [Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options)
69 +- [Vite Vue+TS Template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-vue-ts)
1 +---
2 +title: withDefaults Incorrect Default with Union Types
3 +impact: MEDIUM
4 +impactDescription: fixes incorrect default value behavior with union type props
5 +type: capability
6 +tags: withDefaults, defineProps, union-types, defaults, vue-3.5
7 +---
8 +
9 +# withDefaults Incorrect Default with Union Types
10 +
11 +**Impact: MEDIUM** - fixes spurious "Missing required prop" warning with union type props
12 +
13 +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.
14 +
15 +## Symptoms
16 +
17 +- Vue warns "Missing required prop" despite default being set
18 +- Warning appears only with union types like `false | string`
19 +- TypeScript types are correct
20 +- Runtime value IS correct (the default is applied)
21 +
22 +## Problematic Pattern
23 +
24 +```typescript
25 +// This produces a spurious warning (but works at runtime)
26 +interface Props {
27 + value: false | string // Union type
28 +}
29 +
30 +const props = withDefaults(defineProps<Props>(), {
31 + value: 'default' // Runtime value IS correct, but Vue warns about missing prop
32 +})
33 +```
34 +
35 +## Fix
36 +
37 +**Option 1: Use Reactive Props Destructure (Vue 3.5+)**
38 +```vue
39 +<script setup lang="ts">
40 +interface Props {
41 + value: false | string
42 +}
43 +
44 +// Preferred in Vue 3.5+
45 +const { value = 'default' } = defineProps<Props>()
46 +</script>
47 +```
48 +
49 +**Option 2: Use runtime declaration**
50 +```vue
51 +<script setup lang="ts">
52 +const props = defineProps({
53 + value: {
54 + type: [Boolean, String] as PropType<false | string>,
55 + default: 'default'
56 + }
57 +})
58 +</script>
59 +```
60 +
61 +**Option 3: Split into separate props**
62 +```typescript
63 +interface Props {
64 + enabled: boolean
65 + customValue?: string
66 +}
67 +
68 +const props = withDefaults(defineProps<Props>(), {
69 + enabled: false,
70 + customValue: 'default'
71 +})
72 +```
73 +
74 +## Why Reactive Props Destructure Works
75 +
76 +Vue 3.5's Reactive Props Destructure handles default values at the destructuring level, bypassing the type inference issues with `withDefaults`.
77 +
78 +```typescript
79 +// The default is applied during destructuring, not type inference
80 +const { prop = 'default' } = defineProps<{ prop?: string }>()
81 +```
82 +
83 +## Enable Reactive Props Destructure
84 +
85 +This is enabled by default in Vue 3.5+. For older versions:
86 +```javascript
87 +// vite.config.js
88 +export default {
89 + plugins: [
90 + vue({
91 + script: {
92 + propsDestructure: true
93 + }
94 + })
95 + ]
96 +}
97 +```
98 +
99 +## Reference
100 +
101 +- [vuejs/core#12897](https://github.com/vuejs/core/issues/12897)
102 +- [Reactive Props Destructure RFC](https://github.com/vuejs/rfcs/discussions/502)