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>
197 lines
4.7 KiB
Markdown
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)
|