--- 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` typing - [ ] Prefer `ref()` for simple values to avoid these issues ## The Problem ```vue ``` ## The Solution: Interface Annotation ```vue ``` ## 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 }) // 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 }>() does NOT return { name: Ref } // It returns { name: string } due to automatic unwrapping ``` ## Correct Patterns ### Pattern 1: Simple Interface Annotation ```vue ``` ### Pattern 2: Partial for Optional Fields ```vue ``` ### Pattern 3: Use ref() Instead For simpler cases, prefer `ref()` which has more predictable typing: ```vue ``` ## Generic Composables: Use Ref or shallowRef When working with generic type parameters in composables: ```typescript // PROBLEM: Generic T with ref() causes UnwrapRef issues function useBroken(initial: T) { const state = ref(initial) // Type becomes Ref> state.value = initial // Error: T is not assignable to UnwrapRef return state } // SOLUTION 1: Use explicit Ref type function useFixed1(initial: T) { const state: Ref = ref(initial) as Ref return state } // SOLUTION 2: Use shallowRef (no unwrapping) function useFixed2(initial: T) { const state = shallowRef(initial) // Properly typed as ShallowRef 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([]) const map = reactive>(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)