Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to type refs in Vue with TypeScript

Typing ref() values with TypeScript generics ensures that Vue reactive state has correct types, enabling IDE autocompletion and compile-time error detection for all state access and mutations. As the creator of CoreUI with Vue and TypeScript development experience since 2014, I type every ref in CoreUI Vue components to catch type mismatches early and provide accurate IntelliSense in consuming components. Vue’s TypeScript integration is excellent — in most cases you don’t need to annotate refs explicitly because TypeScript infers the type from the initial value. Explicit type annotations are needed for nullable refs, complex interfaces, and DOM element references.

Type refs with explicit generics when TypeScript cannot infer the type.

import { ref } from 'vue'

// Inferred automatically - no annotation needed
const count = ref(0)           // Ref<number>
const name = ref('')           // Ref<string>
const isOpen = ref(false)      // Ref<boolean>
const items = ref<string[]>([]) // Ref<string[]> - explicit for empty arrays

// Nullable ref - TypeScript needs explicit generic
const selectedId = ref<number | null>(null)  // Ref<number | null>

// Complex object type
interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user'
}

const user = ref<User | null>(null)          // Starts null, will be assigned later
const users = ref<User[]>([])               // Empty array of User objects

// After assignment, TypeScript knows the type
user.value = { id: 1, name: 'Alice', email: '[email protected]', role: 'admin' }
console.log(user.value?.name) // string | undefined (because nullable)

Without the explicit generic on ref<number | null>(null), TypeScript would infer Ref<null> — then assigning a number would be a type error. The generic tells TypeScript what types are valid.

Template Refs with DOM Elements

Type template refs for accessing DOM elements and child components.

<template>
  <input ref="inputEl" type="text" />
  <canvas ref="canvasEl"></canvas>
  <MyDialog ref="dialogRef" />
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { InstanceType } from 'vue'
import MyDialog from './MyDialog.vue'

// DOM element refs - must match HTML element type
const inputEl = ref<HTMLInputElement | null>(null)
const canvasEl = ref<HTMLCanvasElement | null>(null)

// Component instance ref
const dialogRef = ref<InstanceType<typeof MyDialog> | null>(null)

onMounted(() => {
  // TypeScript knows inputEl.value is HTMLInputElement
  inputEl.value?.focus()
  inputEl.value?.select()

  // TypeScript knows the canvas context type
  const ctx = canvasEl.value?.getContext('2d')

  // Access exposed methods from child component
  dialogRef.value?.open()
})
</script>

HTMLInputElement | null is the correct type for template refs — the ref is null before the component mounts. InstanceType<typeof MyDialog> extracts the type of the component instance, giving access to its exposed methods.

Typing ref() in Composables

Type composable return values for consumers.

// useCounter.ts
import { ref, computed, Ref } from 'vue'

interface UseCounterReturn {
  count: Ref<number>
  double: Readonly<Ref<number>>
  increment: () => void
  reset: () => void
}

export function useCounter(initial = 0): UseCounterReturn {
  const count = ref(initial)
  const double = computed(() => count.value * 2)

  return {
    count,
    double,
    increment: () => count.value++,
    reset: () => { count.value = initial }
  }
}

Explicitly typing the return value of a composable documents what it returns and ensures TypeScript catches breaking changes if the implementation changes.

Best Practice Note

In CoreUI Vue components we let TypeScript infer ref types from initial values whenever possible and only add explicit generics for nullable values, empty collections, and DOM references. For component template refs, always use InstanceType<typeof ComponentName> rather than any to maintain type safety on exposed methods. See how to type computed properties in Vue with TypeScript for typing the computed counterpart.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author