'How to correctly declare static refs in Composition API using Typescript?

I am using the Composition API (with <script setup lang='ts'>) to create a ref, used in my template:

const searchRef = ref(null)
onMounted(() => { searchRef.value.focus() })

It does work and my code compiles without errors. The IDE (JetBrains Goland or Webstorm), however, complains with TS2531: Object is possibly 'null'..

The hardcore solution is to use

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Object is possibly 'null'.

but I would like to understand if there is a better way.

I was hoping for optional chaining to be a solution:

const searchRef = ref(null)
onMounted(() => { searchRef.value?.focus() })

The code still compiles and the app still works, but I now have TS2339: Property 'focus' does not exist on type 'never'.

What is the correct way to address this problem?



Solution 1:[1]

For template refs, pass a generic type argument to the ref at initialization. The generic type should match the type of the template ref's targeted element.

Here are some examples of template refs on native HTML elements:

const htmlElemRef = ref<HTMLElement>()         // => type: HTMLElement | undefined
const inputRef = ref<HTMLInputElement>()       // => type: HTMLInputElement | undefined
const textareaRef = ref<HTMLTextAreaElement>() // => type: HTMLTextAreaElement | undefined

The null initialization is optional, and I often omit it for slightly more succinct code. The resulting type is <T | undefined>, which is more correct than null because the ref would actually be undefined if the element did not exist in the template. Optional chaining is still needed for the ref's value (given that it's possibly undefined).

Example:

const searchRef = ref<HTMLInputElement>()
onMounted(() => { searchRef.value?.focus() })

As @Kurt pointed out, template refs on Vue components require different typing. The generic type for the ref would be an InstanceType of the component's type (from a .vue import) (docs):

InstanceType<typeof MyComponentDefinition>

Example:

import HelloWorld from '@/components/HelloWorld.vue'

const helloRef = ref<InstanceType<typeof HelloWorld>>()

Note: To use this with Volar in TypeScript files instead of Vue SFCs, make sure to enable Volar's Takeover Mode.

demo

Solution 2:[2]

One possible way to solve this is to add the proper typings manually:

const searchRef: Ref<HTMLElement | null> = ref(null);
onMounted(() => { searchRef.value?.focus() });

Note: HTMLElement could be different if the the ref is targeting a Vue Component (e.g. Ref<HTMLElement | Vue | null>).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2