In Vue 3 with TypeScript, how do you type a ref that may hold `string | number`, and what does `ref<number>()` without an argument produce?
answer
- inference comes from the initial value
- a type argument overrides inference
- or annotate with the Ref type
- no initial value adds undefined
basics
~20 sPass a type argument, ref<string | number>('2020'), or annotate the variable as Ref<string | number>. Without either, the type is inferred from the initial value only. ref<number>() with no argument is typed Ref<number | undefined>.
solid answer
~40 s`ref()` infers its type from the initial value, so `ref(2020)` is `Ref<number>` and `year.value = '2020'` is a type error. To widen it, either pass a type argument, `const year = ref<string | number>('2020')`, or annotate the variable with the exported `Ref` type, `const year: Ref<string | number> = ref('2020')`. Both give a ref whose `.value` accepts either type. If you pass a type argument but no initial value, `ref<number>()`, the overload returns `Ref<number | undefined>`, because the value really is `undefined` until you assign it; code reading `.value` must handle that. The same inference-first rule applies to `computed()`: its type comes from the getter's return value, and `computed<number>(() => ...)` makes a wrong return a type error.
code
ts · 13 linesimport { ref, computed } from 'vue'
import type { Ref } from 'vue'
interface User { id: number; name: string }
const year = ref<string | number>('2020')
const label: Ref<string> = ref('draft')
const user = ref<User | null>(null)
const page = ref<number>() // Ref<number | undefined>
const greeting = computed<string>(() =>
user.value ? `Hi ${user.value.name}` : 'Hi guest'
)go deeper
Recall that ref infers from its initial value, that ref<T>() or a Ref<T> annotation widens it, and that no initial value adds undefined.
Explain why ref(null) and ref([]) need type arguments, and how computed infers from its getter and is readonly.
Recognise that the declared return type of ref is written with UnwrapRef because nested refs unwrap, and when that surfaces in reusable code.
Set team conventions: null over undefined for unloaded state, no non-null assertions on refs, explicit types on shared composable signatures.
## Inference first In Vue 3, **`ref()`** creates a reactive container whose current value lives in `.value`. With TypeScript, the type of that container is **inferred from the initial value** you pass: ```ts import { ref } from 'vue' const year = ref(2020) // Ref<number> year.value = 2021 // ok year.value = '2020' // error: string is not assignable to number ``` For most refs this is all you need: the initial value already says what the ref holds. Explicit typing matters when the initial value is **narrower than the values the ref will hold later**, or when there is **no meaningful initial value** at all. ## Two ways to widen the type | Technique | Example | Resulting type | |---|---|---| | Type argument | `ref<string \| number>('2020')` | `Ref<string \| number>` | | Variable annotation | `const year: Ref<string \| number> = ref('2020')` | `Ref<string \| number>` | | Plain inference | `ref('2020')` | `Ref<string>` | - The **type argument** is the most common style: it keeps the declaration on one line and reads like the value it guards. - The **annotation** uses the `Ref` type imported with `import type { Ref } from 'vue'`. It is useful when a function parameter or return type must be a ref, for example a composable accepting `Ref<string>`. - In Vue 3.5 the `Ref` interface also has a second parameter for the **write type**, `Ref<T, S = T>`; in everyday code you only write the first. ## `ref<T>()` without an initial value `ref()` has an overload for the no-argument call: `ref<T = any>(): Ref<T | undefined>`. So: ```ts const n = ref<number>() // Ref<number | undefined> n.value?.toFixed(2) // must handle undefined ``` That `undefined` is not TypeScript being pedantic: the value genuinely is `undefined` until something assigns it. Common responses: 1. **Give a real initial value** (`ref(0)`, `ref<User | null>(null)`) when one exists; `null` communicates "not loaded yet" more deliberately than `undefined`. 2. **Narrow before use**: optional chaining, an `if` guard, or a `v-if` in the template. 3. **Avoid non-null assertions** (`n.value!`) unless the logic truly guarantees assignment, because they silence the check without making it true. ## The same rule for `computed()` `computed()` infers a `ComputedRef<T>` from the getter's return type, `computed(() => count.value * 2)` being `ComputedRef<number>`. An explicit argument, `computed<number>(() => ...)`, turns a getter that returns the wrong type into a type error at its definition instead of at every use. A getter-only `ComputedRef` exposes `value` as **readonly**, so assigning to it is a type error; a writable computed takes `{ get, set }` and returns a `WritableComputedRef`. ## Nested refs and unwrapping A ref holding an object makes that object deeply reactive, and **nested refs inside it are unwrapped** in the type as well as at runtime. `ref({ count: ref(0) })` gives `.value.count` as `number`, not `Ref<number>`. That is why the declared return type of `ref()` is written in terms of an `UnwrapRef` helper rather than plain `T`; with concrete types TypeScript evaluates it to the shape you expect, so everyday code never sees it. ## Refs in function signatures The `Ref` type earns its place at **boundaries**, where a value crosses from one function to another. A composable that must react to changes needs a ref, not a snapshot of the value: ```ts import { watch, type Ref } from 'vue' export function useDocumentTitle(title: Ref<string>) { watch(title, (t) => { document.title = t }, { immediate: true }) } ``` - Calling `useDocumentTitle(ref('Home'))` type-checks. - Calling `useDocumentTitle('Home')` is a **type error**: a plain string has no `.value` and would never change, so the signature catches a real bug. - Returning refs from a composable works the same way: an explicit return type such as `{ count: Ref<number> }` documents the contract for every caller and stops an accidental change of type inside the function from leaking out. Inside a single component, inference from the initial value is normally enough; explicit `Ref<T>` annotations pay off on these shared signatures. ## Common mistakes - Writing `ref(null)` for a value that will later be a `User`: inference gives `Ref<null>`, and every later assignment fails. Use `ref<User | null>(null)`. - Writing `ref([])` for a list: an empty array literal gives TypeScript no element type (under strict settings it infers an array of `never`), so pushing an item fails; use `ref<Item[]>([])`. - Annotating with `Ref<number>` but passing a string initial value: the annotation is checked, so it errors. - Reading `.value` of `ref<T>()` as if it were `T`.
- In Vue with TypeScript, why is `const user = ref(null)` a trap for a value that later holds a User?Inference uses the initial value, so the ref is typed `Ref<null>` and assigning a `User` later is a type error. Declare the eventual type up front with `ref<User | null>(null)`, which also documents that `null` means not loaded yet.
- In Vue 3, can you assign to the value of `computed(() => a.value + 1)`?No. A getter-only computed returns a `ComputedRef<T>` whose `value` is readonly, so the assignment is a type error (and a warning at runtime in development). For a settable derived value, pass `{ get, set }` to get a `WritableComputedRef`.
saying these in an interview costs you the question
- Every ref needs an explicit type argument or TypeScript types it as any.
- ref<number>() without an initial value is typed Ref<number>.
- ref(null) will accept a User later because null is assignable to everything.
- The Ref annotation and the ref<T>() argument produce different types.
- A getter-only computed can be assigned through .value like a ref.