In Vue 3, why does the documentation recommend `const book: Book = reactive({...})` over passing a type argument as in `reactive<Book>({...})`?
answer
- reactive's return type is not its input type
- nested refs are unwrapped
- annotate the shape you will read
- interfaces with optional fields
basics
~20 sreactive() returns an unwrapped type, Reactive<T>, which differs from the type argument whenever the object contains refs. Annotating the variable with an interface describes the shape you actually read and write, so the docs advise annotation instead of reactive<T>().
solid answer
~50 s`reactive()` infers its type from the object you pass, and the Vue docs type it explicitly by **annotating the variable**: `interface Book { title: string; year?: number }` then `const book: Book = reactive({ title: 'Vue 3 Guide' })`. They advise against `reactive<Book>()` because the return type is not `Book`: it is `Reactive<Book>`, built on `UnwrapNestedRefs`, which unwraps any refs inside the object. If `T` says a field is a `Ref<number>`, the proxy you get back exposes it as a plain `number`, so the type argument describes the input while your code reads the output. The annotation states the shape you actually use and lets TypeScript check the unwrapped proxy against it, which also gives optional fields like `year?` a declared type from the start. Choosing between `ref` and `reactive` is a separate design question.
go deeper
Recall that the docs type reactive state by annotating the variable with an interface, not by reactive<T>().
Explain that reactive unwraps nested refs, so its return type differs from its input type, and what that means for a type argument.
Spot state interfaces that contain Ref fields and correct them to the unwrapped shape consumers actually read.
Decide how shared state shapes are declared and exported so components and composables agree on one unwrapped interface.
## How `reactive()` is typed **`reactive()`** returns a deep reactive proxy of an object. With no extra help, TypeScript infers the proxy's type from the literal you pass: ```ts import { reactive } from 'vue' const book = reactive({ title: 'Vue 3 Guide' }) // { title: string } ``` That inferred type is often too narrow: a `year` field that the form fills in later does not exist on it, and adding `book.year = 2024` is a type error. So you need a way to say what the object will hold. ## The declared signature In Vue 3.5 the signature is `reactive<T extends object>(target: T): Reactive<T>`, where `Reactive<T>` is built on **`UnwrapNestedRefs<T>`**. The unwrapping mirrors runtime behaviour: a ref stored as a property of a reactive object is read and written **without `.value`**. ```ts import { reactive, ref } from 'vue' const state = reactive({ count: ref(0) }) state.count // number, not Ref<number> state.count++ // updates the inner ref ``` So the type you pass **in** and the type you get **out** are different types whenever the object contains refs. ## Why the type argument misleads With `reactive<T>()`, `T` constrains the **input**. Two consequences follow: 1. If `T` includes a `Ref` field, you write `T` with `Ref<number>` but read the result as `number`. The type you named is not the type you use, and the docs call this out as the reason not to use the generic argument. 2. If `T` has no refs, the argument and the output coincide, but you have still expressed the contract in terms of the input, and a later change that adds a ref field silently changes what readers see. ## What the documentation recommends Annotate the **variable** with an interface describing the shape you read and write: ```ts import { reactive } from 'vue' interface Book { title: string year?: number } const book: Book = reactive({ title: 'Vue 3 Guide' }) book.year = 2024 // ok: declared as optional ``` - The annotation is **checked**: TypeScript verifies that the unwrapped proxy type is assignable to `Book`. - Optional fields such as `year?` are **declared up front**, so later assignments type-check. - Readers see the public shape of the state in one interface, which can be exported and reused by functions that take the state. | Style | What the type describes | Docs advice | |---|---|---| | Inference only | The initial literal | Fine when the literal already has every field | | `const x: Book = reactive(...)` | The shape you read and write | Recommended for explicit typing | | `reactive<Book>(...)` | The input object | Not recommended | ## Arrays, collections and state shapes The annotation style extends naturally to larger state objects, which is where `reactive()` is most often used: ```ts import { reactive } from 'vue' interface Filters { query: string tags: string[] page: number sort?: 'price' | 'date' } const filters: Filters = reactive({ query: '', tags: [], page: 1 }) filters.sort = 'price' // ok filters.sort = 'name' // error: not in the union ``` 1. The empty `tags: []` literal would infer poorly on its own; the annotation gives it `string[]`. 2. A **literal union** such as `'price' | 'date'` is only available through a declared type; inference would widen `'price'` to `string`. 3. The same `Filters` interface can type a function parameter, `function toQuery(f: Filters)`, which then accepts the reactive object directly because its unwrapped type matches. ## The same idea for `computed()` `computed()` follows the inference-first rule too: `computed(() => book.title.length)` is a `ComputedRef<number>`. You can pass an explicit type, `computed<number>(() => ...)`, and a getter that returns something else becomes a type error at the definition. A getter-only computed's `value` is readonly in its type. ## Pitfalls - **Annotating with a type that has `Ref` fields**: the proxy exposes them unwrapped, so the annotation will not match what you read. - **Typing the literal instead of the variable**: `reactive({ title: 'x' } as Book)` asserts rather than checks, so a missing required field slips through; the annotation form checks it. - **Passing a primitive**: `reactive(0)` fails the `T extends object` constraint; primitives belong in `ref()`. - **Expecting annotation to change runtime behaviour**: it is types only; the proxy is identical either way.
- In Vue 3, what type does `reactive({ count: ref(0) }).count` have, and why?`number`. A ref nested as a property of a reactive object is unwrapped at runtime, and the `Reactive<T>` return type mirrors that through `UnwrapNestedRefs`. You read and write `state.count` directly; there is no `.value`.
- How do you give a Vue computed an explicit type, and what does that buy you?Pass a type argument, `computed<number>(() => ...)`. The getter is then checked against it, so returning the wrong type fails at the definition rather than wherever the computed is read. Without it, the type is inferred from the getter's return value, which is usually enough.
saying these in an interview costs you the question
- reactive<Book>() returns exactly Book, so it is the clearest way to type state.
- A Ref field inside a reactive object still needs .value when read.
- Annotating the variable changes how Vue builds the reactive proxy.
- reactive() accepts primitives like a number just as ref() does.
- reactive can only be typed through its generic argument, never by annotation.