---
title: "Typing refs, computed and inject · vuejs.school"
description: "Give ref, computed, template refs and inject the right types in Vue 3.5, and fix the possibly null errors without a single exclamation mark."
canonical_url: "https://vuejs.school/community/blog/typing-refs-computed-and-inject"
meta:
  "og:description": "Give ref, computed, template refs and inject the right types in Vue 3.5, and fix the possibly null errors without a single exclamation mark."
  "og:image": "https://vuejs.school/_og/d/a_~VHlwaW5nIHJlZnMsIGNvbXB1dGVkIGFuZCBpbmplY3QgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~VHlwaW5nIHJlZnMsIGNvbXB1dGVkIGFuZCBpbmplY3Q%2Cdescription_~R2l2ZSByZWYsIGNvbXB1dGVkLCB0ZW1wbGF0ZSByZWZzIGFuZCBpbmplY3QgdGhlIHJpZ2h0IHR5cGVzIGluIFZ1ZSAzLjUsIGFuZCBmaXggdGhlIHBvc3NpYmx5IG51bGwgZXJyb3JzIHdpdGhvdXQgYSBzaW5nbGUgZXhjbGFtYXRpb24gbWFyay4%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBpbmctcmVmcy1jb21wdXRlZC1hbmQtaW5qZWN0Ig%2Cs_ivD-M2rUnASqJuSa.png"
  "og:image:alt": "Typing refs, computed and inject · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Typing refs, computed and inject · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~VHlwaW5nIHJlZnMsIGNvbXB1dGVkIGFuZCBpbmplY3QgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~VHlwaW5nIHJlZnMsIGNvbXB1dGVkIGFuZCBpbmplY3Q%2Cdescription_~R2l2ZSByZWYsIGNvbXB1dGVkLCB0ZW1wbGF0ZSByZWZzIGFuZCBpbmplY3QgdGhlIHJpZ2h0IHR5cGVzIGluIFZ1ZSAzLjUsIGFuZCBmaXggdGhlIHBvc3NpYmx5IG51bGwgZXJyb3JzIHdpdGhvdXQgYSBzaW5nbGUgZXhjbGFtYXRpb24gbWFyay4%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBpbmctcmVmcy1jb21wdXRlZC1hbmQtaW5qZWN0Ig%2Cs_ivD-M2rUnASqJuSa.png"
  "twitter:image:alt": "Typing refs, computed and inject · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Let ref infer the type, or pass a union

`ref(0)` is a `Ref<number>`; `ref('')` is a `Ref<string>`. Assign the wrong type and you get the error at the assignment, which is where you want it:

```
import { ref } from 'vue'

const count = ref(0)
count.value = 'ten'
// Type 'string' is not assignable to type 'number'.
```

When a value can legitimately hold more than one type, say an ID that arrives as a string from the URL and is stored as a number, give `ref` a generic argument with the union. The annotation form `const id: Ref<string | number>` does the same, but the generic reads better and needs no extra import.

```
const id = ref<string | number>('42')
id.value = 42
```

## Handle a ref that starts empty without `!`

The common case: data that is not there yet. `ref<User>()` with no argument is typed `Ref<User | undefined>`. `ref<User | null>(null)` is typed `Ref<User | null>`. Pick one convention and keep it; this post uses `null` for "nothing loaded yet".

Either way, every read now has to deal with the empty state. This is where the "possibly null" error comes from, and the tempting fix is a `!`:

*composables/useInitials.ts**Breaks***

```
import { computed, ref } from 'vue'

interface User {
  id: number
  name: string
}

const user = ref<User | null>(null)

const initials = computed(() => user.value.name.slice(0, 2))
// 'user.value' is possibly 'null'. (18047)
```

The non-null assertion, `user.value!.name`, makes the error go away and keeps the crash: if a template reads `initials` before the user has loaded, the getter throws `Cannot read properties of null`. Say what should happen in the empty case instead. Optional chaining and a default do it in one line:

*composables/useInitials.ts**Works***

```
const initials = computed(() => user.value?.name.slice(0, 2) ?? '')
```

In templates, `v-if="user"` narrows the type inside the block, so `{{ user.name }}` under it compiles with no `?.`. In functions, an early `if (!user.value) return` does the same for the lines that follow.

## Type composable parameters with Ref or MaybeRefOrGetter

Once you write composables, functions that bundle reactive logic for reuse, the question flips: what type should a parameter have? It depends on what the composable does with the value:

- If it writes to the value, ask for a `Ref<T>`.
- If it only reads, accept more. `MaybeRef<T>` takes a plain value or a ref, and `MaybeRefOrGetter<T>` also takes a getter function, `() => T`.

Pair either with `toValue()`, which unwraps whichever you got. This composable sets the page title:

*composables/useTitle.ts*

```
import { watchEffect, toValue, type MaybeRefOrGetter } from 'vue'

export function useTitle(title: MaybeRefOrGetter<string>) {
  watchEffect(() => {
    document.title = toValue(title)
  })
}
```

Callers can now pass a plain string, a ref, or a getter such as `() => post.value.title`. Because `toValue()` is called inside `watchEffect`, the effect tracks what the ref or getter reads and re-runs when it changes. `MaybeRef`, `MaybeRefOrGetter` and `toValue()` arrived in Vue 3.3, so any 3.5 project has them.

## When to give computed an explicit type

`computed()` takes its type from what the getter returns. `const double = computed(() => count.value * 2)` is a `ComputedRef<number>`, so `double.value.split('')` is an error. You rarely annotate it, with two exceptions:

- **A getter with several branches.** An explicit `computed<Status>(() => ...)` checks every value the getter returns against `Status`. A typo such as `'laoding'` in one branch fails on the `computed()` call itself, with `Type '"laoding"' is not assignable to type 'Status'` in the message, rather than wherever the result is used later.
- **A writable computed.** One with `get` and `set` is a `WritableComputedRef<T>`, the type you name when you pass it to a function that expects one.

Here the generic limits the getter to the three states of `Status`:

```
import { computed, ref } from 'vue'

type Status = 'idle' | 'loading' | 'done'

const loading = ref(false)
const loaded = ref(false)

const status = computed<Status>(() => {
  if (loading.value) return 'loading'
  return loaded.value ? 'done' : 'idle'
})
```

## Type template refs with useTemplateRef

Vue 3.5 added `useTemplateRef('name')`, which returns a read-only shallow ref to the element or component whose `ref="name"` attribute matches.

When the `ref` attribute is static and both sides are in the same single-file component, the Vue language tools (2.1 and later, which power `vue-tsc` and the editor) infer the type from the element. Outside that case, or when you want the type written down, pass it as the generic, as `SearchBox.vue` below does. Either way the value is `null` until the component has mounted, and again if a `v-if` removes the element, so read it in `onMounted` or later, with `?.`.

*components/SearchBox.vue*

```
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'

const input = useTemplateRef<HTMLInputElement>('search')

onMounted(() => {
  input.value?.focus()
})
</script>

<template>
  <input ref="search" type="search" placeholder="Search" />
</template>
```

For a child component, the type is the component's instance, which TypeScript can extract with `InstanceType<typeof Modal>`. Of the child's own variables and functions, only those it exposes through `defineExpose` are on that type, so a call to `modal.value?.open()` compiles only when `Modal.vue` exposes it with `defineExpose({ open })`.

*components/Settings.vue*

```
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Modal from './Modal.vue'

const modal = useTemplateRef<InstanceType<typeof Modal>>('modal')

function openSettings() {
  modal.value?.open()
}
</script>

<template>
  <button type="button" @click="openSettings">Settings</button>
  <Modal ref="modal" />
</template>
```

Before 3.5 the same thing was `const input = ref<HTMLInputElement | null>(null)` with a matching `ref="input"` in the template. It still works, and you will see it in older code.

## Type provide and inject with an InjectionKey

`provide()` and `inject()` pass a value down the component tree without props. With a string key, `inject('theme')` has type `unknown`, because nothing links the two ends. Vue's `InjectionKey<T>` is a symbol that carries the type: declare it once in a shared file, and both `provide` and `inject` check against it.

*keys.ts*

```
import type { InjectionKey, Ref } from 'vue'

export interface Theme {
  mode: 'light' | 'dark'
}

export const themeKey = Symbol('theme') as InjectionKey<Ref<Theme>>
```

Start with the provider. Somewhere above the consumers, usually `App.vue`, a component creates the ref and provides it under the key:

*App.vue*

```
<script setup lang="ts">
import { provide, ref } from 'vue'
import { themeKey, type Theme } from './keys'

const theme = ref<Theme>({ mode: 'light' })
provide(themeKey, theme)
</script>
```

The provider now gets an error if it provides anything but a `Ref<Theme>`. Provide a plain object instead of the ref and the error appears on the `provide` call, which is where the mistake is.

The consumer still gets `Ref<Theme> | undefined` from `inject(themeKey)`, because TypeScript cannot know that an ancestor called `provide`. There are two honest ways to remove the `undefined`: throw a clear error, or pass a default (the next section). Throwing suits a component that cannot do its job without the provider, such as a form field outside a form or a tab outside a tab group. The `throw` also narrows the type for the rest of the function:

*composables/useTheme.ts*

```
import { inject } from 'vue'
import { themeKey } from '../keys'

export function useTheme() {
  const theme = inject(themeKey)
  if (!theme) {
    throw new Error('useTheme() needs a ThemeProvider above the component')
  }
  return theme
}
```

The third option, `inject(themeKey) as Ref<Theme>`, is a promise to the compiler that you cannot keep from inside the component. When a provider is missing, Vue only logs `injection "Symbol(theme)" not found` in development, and the code fails later, at the first read of `.value`, far from the cause. The thrown error names the fix instead.

## Give inject a default when a fallback is safe

A theme is different from a form field. A component rendered on its own, in a test or in a playground, can fall back to light mode and nothing breaks. For that case `inject()` takes a default as its second argument, and the return type loses its `undefined`.

The default must match the key's type, which here is a `Ref<Theme>`, so the fallback is a ref too. Pass a factory and `true` as the third argument, so the ref is only created when no provider exists:

*composables/useTheme.ts*

```
import { inject, ref } from 'vue'
import { themeKey, type Theme } from '../keys'

export function useTheme() {
  return inject(themeKey, () => ref<Theme>({ mode: 'light' }), true)
}
```

The result is a `Ref<Theme>`, so `useTheme().value.mode` compiles with no `?.`. Leave out the `true` and Vue treats the function itself as the default, which TypeScript rejects, because a function is not a `Ref<Theme>`. The error is a long one, but it lands on the `inject` line.

The provider in `App.vue` does not change. A default has one cost: it is a fresh ref that no provider will ever update, so a consumer that falls back to it is cut off from the rest of the app. Choose the default when a missing provider is a legitimate state, and the throw when it is a bug.

**Check it stuck**3 questions from the Typing refs, computed and inject quiz.

[**Practice now**](https://vuejs.school/learn/typescript/typing-refs-computed-and-inject?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 6 min read**TypeScript strict mode in a Vue project: the errors you'll hit and the fixes**](https://vuejs.school/community/blog/typescript-strict-mode-in-a-vue-project) [Release notes · 7 min read**TypeScript 7.0: the Go compiler lands, and what it means for vue-tsc**](https://vuejs.school/community/blog/typescript-7-the-go-compiler-lands)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/typing-refs-computed-and-inject#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-05-27","description":"Give ref, computed, template refs and inject the right types in Vue 3.5, and fix the possibly null errors without a single exclamation mark.","name":"Typing refs, computed and inject","url":"https://vuejs.school/community/blog/typing-refs-computed-and-inject","breadcrumb":{"@id":"https://vuejs.school/community/blog/typing-refs-computed-and-inject#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/typing-refs-computed-and-inject"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/typing-refs-computed-and-inject#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-05-27","description":"Give ref, computed, template refs and inject the right types in Vue 3.5, and fix the possibly null errors without a single exclamation mark.","headline":"Typing refs, computed and inject","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~VHlwaW5nIHJlZnMsIGNvbXB1dGVkIGFuZCBpbmplY3QgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~VHlwaW5nIHJlZnMsIGNvbXB1dGVkIGFuZCBpbmplY3Q%2Cdescription_~R2l2ZSByZWYsIGNvbXB1dGVkLCB0ZW1wbGF0ZSByZWZzIGFuZCBpbmplY3QgdGhlIHJpZ2h0IHR5cGVzIGluIFZ1ZSAzLjUsIGFuZCBmaXggdGhlIHBvc3NpYmx5IG51bGwgZXJyb3JzIHdpdGhvdXQgYSBzaW5nbGUgZXhjbGFtYXRpb24gbWFyay4%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBpbmctcmVmcy1jb21wdXRlZC1hbmQtaW5qZWN0Ig%2Cs_ivD-M2rUnASqJuSa.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/typing-refs-computed-and-inject#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/typing-refs-computed-and-inject#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/typing-refs-computed-and-inject#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Typing refs, computed and inject","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~VHlwaW5nIHJlZnMsIGNvbXB1dGVkIGFuZCBpbmplY3QgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~VHlwaW5nIHJlZnMsIGNvbXB1dGVkIGFuZCBpbmplY3Q%2Cdescription_~R2l2ZSByZWYsIGNvbXB1dGVkLCB0ZW1wbGF0ZSByZWZzIGFuZCBpbmplY3QgdGhlIHJpZ2h0IHR5cGVzIGluIFZ1ZSAzLjUsIGFuZCBmaXggdGhlIHBvc3NpYmx5IG51bGwgZXJyb3JzIHdpdGhvdXQgYSBzaW5nbGUgZXhjbGFtYXRpb24gbWFyay4%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBpbmctcmVmcy1jb21wdXRlZC1hbmQtaW5qZWN0Ig%2Cs_ivD-M2rUnASqJuSa.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~VHlwaW5nIHJlZnMsIGNvbXB1dGVkIGFuZCBpbmplY3QgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~VHlwaW5nIHJlZnMsIGNvbXB1dGVkIGFuZCBpbmplY3Q%2Cdescription_~R2l2ZSByZWYsIGNvbXB1dGVkLCB0ZW1wbGF0ZSByZWZzIGFuZCBpbmplY3QgdGhlIHJpZ2h0IHR5cGVzIGluIFZ1ZSAzLjUsIGFuZCBmaXggdGhlIHBvc3NpYmx5IG51bGwgZXJyb3JzIHdpdGhvdXQgYSBzaW5nbGUgZXhjbGFtYXRpb24gbWFyay4%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBpbmctcmVmcy1jb21wdXRlZC1hbmQtaW5qZWN0Ig%2Cs_ivD-M2rUnASqJuSa.png"}]}
```