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>
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user