skip to content

In a Vue 3.5 `<script setup lang="ts">` component, how do you type template refs to an `<input>` element and to a child component?

level: middleimportance: must knowfreq 55%

answer

  1. useTemplateRef since 3.5
  2. static refs can be inferred
  3. null before mount and after v-if
  4. InstanceType of typeof the component
  5. ComponentPublicInstance as the loose fallback

basics

~10 s

Use useTemplateRef: the Vue 3.5 language tools infer static refs, or you write useTemplateRef<HTMLInputElement>('input'). For a component, use InstanceType<typeof Child>. The value is null until mount, so access it with optional chaining.

solid answer

~50 s

In Vue 3.5, `useTemplateRef('input')` returns a readonly shallow ref typed `T | null`, bound to the element whose `ref` attribute has that key. With `@vue/language-tools` 2.1+ (the editor and `vue-tsc`), a **static** ref's type is inferred from the element or component it sits on, so often you write no type at all. Where inference cannot work, such as a dynamic `<component :is>` or code outside an SFC, pass the type: `useTemplateRef<HTMLInputElement>('input')` for a DOM node, and `useTemplateRef<InstanceType<typeof Child>>('child')` for a component, whose instance type includes what the child exposes. Use `ComponentPublicInstance` when the exact component does not matter. Either way the value is `null` before mount and again after a `v-if` removes the node, so read it as `input.value?.focus()` inside `onMounted` or later. Before 3.5 the idiom was `ref<HTMLInputElement | null>(null)` named like the attribute.

go deeper

for a junior

Recall useTemplateRef with a DOM interface type argument and optional chaining because the value starts as null.

for a middle

Explain InstanceType<typeof Child> for component refs, when the language tools infer ref types, and why the ref is null before mount and after v-if.

for a senior

Handle dynamic components with unions or ComponentPublicInstance, avoid non-null assertions, and keep exposed child APIs typed through the instance type.

for a principal

Limit template-ref coupling across components, preferring props and events, so typed refs stay the exception for imperative DOM work.

## What a template ref holds A **template ref** gives script code direct access to a DOM element or a child component instance rendered by the template. In Vue 3.5 the API is **`useTemplateRef(key)`**, which returns a ref that Vue fills when the element with `ref="key"` is mounted. Its declared type is `Readonly<ShallowRef<T | null>>`: - **readonly**: Vue sets it; your code only reads it; - **shallow**: the element or instance itself is stored, not a deep reactive copy; - **`T | null`**: it is `null` until mount, and set back to `null` when the node is removed. ## Typing a DOM element ref ```vue <script setup lang="ts"> import { useTemplateRef, onMounted } from 'vue' const input = useTemplateRef<HTMLInputElement>('search') onMounted(() => { input.value?.focus() }) </script> <template> <input ref="search" type="search" /> </template> ``` 1. The type argument names the **DOM interface** of the element: `HTMLInputElement`, `HTMLCanvasElement`, `HTMLDialogElement` and so on. 2. **Optional chaining** (or a guard) is required for strict type safety, because of the `null` phases. 3. With Vue 3.5 and `@vue/language-tools` 2.1 or later, the language tools **infer** the type for a static `ref="search"` from the element it is on, so the explicit argument is often unnecessary. It remains the fallback where inference is impossible. ## Typing a component ref For a child component, the type is the component's **instance type**, obtained with TypeScript's `InstanceType` over `typeof` the imported component: ```vue <script setup lang="ts"> import { useTemplateRef } from 'vue' import Foo from './Foo.vue' import Bar from './Bar.vue' type FooType = InstanceType<typeof Foo> type BarType = InstanceType<typeof Bar> const comp = useTemplateRef<FooType | BarType>('comp') </script> <template> <component :is="Math.random() > 0.5 ? Foo : Bar" ref="comp" /> </template> ``` - The instance type includes the public instance properties and whatever a `<script setup>` child published with `defineExpose`; everything else in a `<script setup>` child is closed. - A dynamic `<component :is>` is a case where **inference cannot pick one component**, so an explicit union is the documented approach. - When the exact component is unknown or irrelevant, **`ComponentPublicInstance`** types only what every component shares, such as `$el`. - A **generic** child component is the exception: `InstanceType` does not work there and a different helper is needed. ## Summary table | Target | Explicit type | Value before mount | |---|---|---| | DOM element | `useTemplateRef<HTMLInputElement>('key')` | `null` | | Known component | `useTemplateRef<InstanceType<typeof Child>>('key')` | `null` | | Any component | `useTemplateRef<ComponentPublicInstance>('key')` | `null` | | Static ref, language-tools 2.1+ | usually none: inferred | `null` | ## Before Vue 3.5 Older code declares a plain ref with the **same name** as the `ref` attribute and an explicit nullable type: ```ts const el = ref<HTMLInputElement | null>(null) // template: <input ref="el" /> ``` The typing rules are the same: DOM interface or `InstanceType`, `null` initial value, optional chaining. `useTemplateRef` removes the name coupling between variable and attribute and gives the ref a readonly type. ## Where the typed value is used Typed template refs mostly serve **imperative DOM work** that the template cannot express: - focusing or selecting an input after a dialog opens; - measuring an element (`getBoundingClientRect()`) for a popover position; - calling `showModal()` on an `HTMLDialogElement`; - handing a canvas to a charting or drawing routine. Each needs the **specific DOM interface**, which is why `HTMLElement` is not enough: `showModal()` exists only on `HTMLDialogElement`, `getContext()` only on `HTMLCanvasElement`. A watcher is a common consumer too: `watch(input, el => el?.focus())` runs when the element appears, and the `null` in the type forces the guard that the unmount case needs. ## Pitfalls - **Reading the ref during `setup`**: it is still `null`; use `onMounted` or later. - **Non-null assertions** (`input.value!.focus()`): they compile but crash if a `v-if` has removed the element. - **Using the wrong DOM interface**: `HTMLElement` compiles but hides `value`, `select()` and friends that `HTMLInputElement` has. - **Calling `useTemplateRef` twice with the same key** in one component: Vue warns in development that the key already exists. - **Typing the ref as the component's props type**: the ref holds the mounted instance, so the type must be the instance type, not the props interface. - **Leaving `any` from an old `ref(null)`**: `ref(null)` without a type argument is `Ref<null>`, so older code often added `any` to make it compile; replacing it with `useTemplateRef` and a proper type restores checking. - **Forgetting that refs inside `v-for` hold arrays**: the element type then becomes an array of the element interface.

  • Why does a Vue template ref's type include null even inside a component that always renders the element?
    The value is `null` until the component mounts, so any read during `setup` sees `null`, and it is reset to `null` when the element unmounts, for example through `v-if`. The type reflects both phases, which is why the docs ask for optional chaining or a type guard.
  • When does a Vue 3.5 project still need an explicit type argument on useTemplateRef?
    When the language tools cannot infer the target: a dynamic `<component :is>`, refs created outside an SFC, or a key that is not a static `ref` attribute in this template. In a plain static case with `@vue/language-tools` 2.1+, the type is inferred from the element or component.

A template ref is a reserved-seat card: the seat is typed for a particular guest (the element or component), but it stays empty until the guest arrives at mount and is cleared again when they leave, so you always check before speaking to the seat.

saying these in an interview costs you the question

  • A template ref is available as soon as setup runs.
  • useTemplateRef<HTMLInputElement>() gives a value of type HTMLInputElement, never null.
  • A component ref's type is just the component's props type.
  • HTMLElement is as good as HTMLInputElement for an input's ref.
  • Static template refs must always be typed by hand in Vue 3.5.