Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/ts-reactive-no-generic-argument.md
1---2title: Do Not Use Generic Argument with reactive()3impact: MEDIUM4impactDescription: The generic argument type differs from the actual return type due to ref unwrapping, causing type mismatches5type: gotcha6tags: [vue3, typescript, reactive, ref-unwrapping, composition-api]7---89# Do Not Use Generic Argument with reactive()1011**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.1213## Task Checklist1415- [ ] Use type annotation on the variable, not generic argument16- [ ] Understand that `reactive()` unwraps nested refs17- [ ] For generic composables, use `shallowRef` or explicit `Ref<T>` typing18- [ ] Prefer `ref()` for simple values to avoid these issues1920## The Problem2122```vue23<script setup lang="ts">24import { reactive, ref } from 'vue'2526interface Book {27title: string28year: number29author: Ref<string> // Nested ref30}3132// WRONG: Generic argument doesn't account for ref unwrapping33const book = reactive<Book>({34title: 'Vue 3 Guide',35year: 2024,36author: ref('John Doe')37})3839// TypeScript thinks book.author is Ref<string>40// But at runtime, it's unwrapped to just string!41book.author.value // TypeScript: OK, Runtime: ERROR (author is a string, not a ref)42</script>43```4445## The Solution: Interface Annotation4647```vue48<script setup lang="ts">49import { reactive, ref } from 'vue'5051interface Book {52title: string53year?: number54}5556// CORRECT: Annotate the variable, not the generic57const book: Book = reactive({58title: 'Vue 3 Guide'59})6061book.title = 'New Title' // TypeScript knows this is string62book.year = 2024 // TypeScript knows this is number | undefined63</script>64```6566## Why This Happens6768When you use `reactive()`, Vue automatically unwraps any nested refs:6970```typescript71import { reactive, ref, Ref } from 'vue'7273const name = ref('John')74const state = reactive({75name: name // This is a Ref<string>76})7778// At runtime, state.name is 'John' (string), NOT a Ref79console.log(state.name) // 'John' (not ref object)80console.log(state.name.value) // Runtime error: .value doesn't exist8182// The ACTUAL return type is different from what you'd expect83// reactive<{ name: Ref<string> }>() does NOT return { name: Ref<string> }84// It returns { name: string } due to automatic unwrapping85```8687## Correct Patterns8889### Pattern 1: Simple Interface Annotation9091```vue92<script setup lang="ts">93interface FormState {94name: string95email: string96age: number97}9899const form: FormState = reactive({100name: '',101email: '',102age: 0103})104</script>105```106107### Pattern 2: Partial for Optional Fields108109```vue110<script setup lang="ts">111interface User {112id: string113name: string114email: string115}116117// Start with partial data118const user: Partial<User> = reactive({})119120// Fill in later121user.id = '123'122user.name = 'John'123</script>124```125126### Pattern 3: Use ref() Instead127128For simpler cases, prefer `ref()` which has more predictable typing:129130```vue131<script setup lang="ts">132interface User {133id: string134name: string135}136137// ref() works well with generics138const user = ref<User>({139id: '1',140name: 'John'141})142143// Access with .value - clear and predictable144user.value.name = 'Jane'145</script>146```147148## Generic Composables: Use Ref<T> or shallowRef149150When working with generic type parameters in composables:151152```typescript153// PROBLEM: Generic T with ref() causes UnwrapRef issues154function useBroken<T>(initial: T) {155const state = ref(initial) // Type becomes Ref<UnwrapRef<T>>156state.value = initial // Error: T is not assignable to UnwrapRef<T>157return state158}159160// SOLUTION 1: Use explicit Ref<T> type161function useFixed1<T>(initial: T) {162const state: Ref<T> = ref(initial) as Ref<T>163return state164}165166// SOLUTION 2: Use shallowRef (no unwrapping)167function useFixed2<T>(initial: T) {168const state = shallowRef(initial) // Properly typed as ShallowRef<T>169return state170}171```172173## When Generic Argument IS Safe174175For simple non-ref values without nested reactivity, the generic is safe:176177```typescript178// Safe: no nested refs179const state = reactive<{ count: number; name: string }>({180count: 0,181name: ''182})183184// Also safe: explicit simple types185const list = reactive<string[]>([])186const map = reactive<Map<string, number>>(new Map())187```188189The issue only arises when:1901. You have nested Ref types in your interface1912. You're using generic type parameters that might contain refs192193## Reference194- [Vue.js TypeScript with Composition API - Typing reactive()](https://vuejs.org/guide/typescript/composition-api.html#typing-reactive)195- [GitHub Issue: ref with generic type](https://github.com/vuejs/core/discussions/9564)196- [Vue TypeScript Caveats Gist](https://gist.github.com/LinusBorg/e041ff635994b50b7cec9383c3a067f1)197