Files
agent-skills/skills/vue-best-practices/reference/ts-reactive-no-generic-argument.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

197 lines
4.7 KiB
Markdown

---
title: Do Not Use Generic Argument with reactive()
impact: MEDIUM
impactDescription: The generic argument type differs from the actual return type due to ref unwrapping, causing type mismatches
type: gotcha
tags: [vue3, typescript, reactive, ref-unwrapping, composition-api]
---
# Do Not Use Generic Argument with reactive()
**Impact: MEDIUM** - It is NOT recommended to use the generic argument of `reactive()` because the returned type, which handles nested ref unwrapping, is different from the generic argument type. Use interface annotation on the variable instead.
## Task Checklist
- [ ] Use type annotation on the variable, not generic argument
- [ ] Understand that `reactive()` unwraps nested refs
- [ ] For generic composables, use `shallowRef` or explicit `Ref<T>` typing
- [ ] Prefer `ref()` for simple values to avoid these issues
## The Problem
```vue
<script setup lang="ts">
import { reactive, ref } from 'vue'
interface Book {
title: string
year: number
author: Ref<string> // Nested ref
}
// WRONG: Generic argument doesn't account for ref unwrapping
const book = reactive<Book>({
title: 'Vue 3 Guide',
year: 2024,
author: ref('John Doe')
})
// TypeScript thinks book.author is Ref<string>
// But at runtime, it's unwrapped to just string!
book.author.value // TypeScript: OK, Runtime: ERROR (author is a string, not a ref)
</script>
```
## The Solution: Interface Annotation
```vue
<script setup lang="ts">
import { reactive, ref } from 'vue'
interface Book {
title: string
year?: number
}
// CORRECT: Annotate the variable, not the generic
const book: Book = reactive({
title: 'Vue 3 Guide'
})
book.title = 'New Title' // TypeScript knows this is string
book.year = 2024 // TypeScript knows this is number | undefined
</script>
```
## Why This Happens
When you use `reactive()`, Vue automatically unwraps any nested refs:
```typescript
import { reactive, ref, Ref } from 'vue'
const name = ref('John')
const state = reactive({
name: name // This is a Ref<string>
})
// At runtime, state.name is 'John' (string), NOT a Ref
console.log(state.name) // 'John' (not ref object)
console.log(state.name.value) // Runtime error: .value doesn't exist
// The ACTUAL return type is different from what you'd expect
// reactive<{ name: Ref<string> }>() does NOT return { name: Ref<string> }
// It returns { name: string } due to automatic unwrapping
```
## Correct Patterns
### Pattern 1: Simple Interface Annotation
```vue
<script setup lang="ts">
interface FormState {
name: string
email: string
age: number
}
const form: FormState = reactive({
name: '',
email: '',
age: 0
})
</script>
```
### Pattern 2: Partial for Optional Fields
```vue
<script setup lang="ts">
interface User {
id: string
name: string
email: string
}
// Start with partial data
const user: Partial<User> = reactive({})
// Fill in later
user.id = '123'
user.name = 'John'
</script>
```
### Pattern 3: Use ref() Instead
For simpler cases, prefer `ref()` which has more predictable typing:
```vue
<script setup lang="ts">
interface User {
id: string
name: string
}
// ref() works well with generics
const user = ref<User>({
id: '1',
name: 'John'
})
// Access with .value - clear and predictable
user.value.name = 'Jane'
</script>
```
## Generic Composables: Use Ref<T> or shallowRef
When working with generic type parameters in composables:
```typescript
// PROBLEM: Generic T with ref() causes UnwrapRef issues
function useBroken<T>(initial: T) {
const state = ref(initial) // Type becomes Ref<UnwrapRef<T>>
state.value = initial // Error: T is not assignable to UnwrapRef<T>
return state
}
// SOLUTION 1: Use explicit Ref<T> type
function useFixed1<T>(initial: T) {
const state: Ref<T> = ref(initial) as Ref<T>
return state
}
// SOLUTION 2: Use shallowRef (no unwrapping)
function useFixed2<T>(initial: T) {
const state = shallowRef(initial) // Properly typed as ShallowRef<T>
return state
}
```
## When Generic Argument IS Safe
For simple non-ref values without nested reactivity, the generic is safe:
```typescript
// Safe: no nested refs
const state = reactive<{ count: number; name: string }>({
count: 0,
name: ''
})
// Also safe: explicit simple types
const list = reactive<string[]>([])
const map = reactive<Map<string, number>>(new Map())
```
The issue only arises when:
1. You have nested Ref types in your interface
2. You're using generic type parameters that might contain refs
## Reference
- [Vue.js TypeScript with Composition API - Typing reactive()](https://vuejs.org/guide/typescript/composition-api.html#typing-reactive)
- [GitHub Issue: ref with generic type](https://github.com/vuejs/core/discussions/9564)
- [Vue TypeScript Caveats Gist](https://gist.github.com/LinusBorg/e041ff635994b50b7cec9383c3a067f1)