Subscribe on changes!

Component $refs type should be more narrow

avatar
Jun 28th 2022

Vue version

3.2.37

Link to minimal reproduction

https://stackblitz.com/edit/vitejs-vite-fpuqmq?file=package.json

Steps to reproduce

Build a component using defineComponent, typescript and the options api with at template ref, then try to use that template ref.

What is expected?

The type of that template ref will be something reasonable like undefined | HTMLElement or undefined | HTMLElement | DefineComponent or any instead of unknown.

What is actually happening?

$refs is a Record<string, unknown>, forcing a cast whenever using it.

System Info

No response

Any additional comments?

No response

avatar
Jun 28th 2022

With the proposed type you would also be forced to cast it pretty much whenever using it, wouldn't you?

  • want to access a property of an HTMLElement? property does not exist on DefineComponent -> need to cast
  • want to access a component API method, like $emit? property does not exist on HTMLElement -> need to cast
  • want to access a property of a specific component? property does not exist on HTMLElement nor DefineComponent -> need to cast

Sure, we could use any, but TS best practices usually recommend against so implicitly weak APIs, leading people to write code that is prone to typo errors and other mistakes TS is usually meant to protect you from.

Tip: even if you prefer Options API: use composition API just for template refs:

export defineComponent() {
  setup() {
    return { el: ref<HTMLElement>() }
  },{
  mounted() {
    this.el // => HTMLElment | undefined
  }
}
avatar
Jun 28th 2022

Yes, but would better inform the user what is going in in the situation. If I see "Property 'innerText' does not exist on type 'DefineComponent | HTMLElement': Property 'innerText' does not exist on type 'DefineComponent'." I'll immediate realize what the problem is and what I need to cast too.

"Object is of type 'unknown'." Provides no information about what the issue is and what the fix is. Most people are probably just going to cast it to any anyhow. Others who like the type safety but aren't experts are going to have to google to figure out what the type of an HTMLElement. The truly neurotic might even end up bugging people on github about it.

It also allows someone to write better type guards to sort out the difference, should someone end up in the unlikely situation of need to us the template ref but not knowing what it is.

Also realizing it should probably be Element instead of HTMLElement, can't forget about SVGElement.

avatar
Jun 29th 2022

I see your point. It could be a worthwhile enhancement, but I'd be worried that it might break existing code in userland now, as it's not guaranteed that the typecasting people do now would be compatible with HTMLElement | DefineComponent. People might be doing this right now:

(this.$refs.myComponent as { theOneProperyImInterestedIn: string }).theOnePropertyImInterestedIn = 'new value'

which, with your proposed type, would now break. People would then have to do:

(this.$refs.myComponent as unknown as { theOneProperyImInterestedIn: string }).theOnePropertyImInterestedIn = 'new value'

So technically, this is a breaking change.

Also, we would have to create this type in a way that it was extendable as Element would only make sense in the context of runtime-dom, while ptojects like troisjs provide their own renderer, in which refs could point to things such as a ThreeJS scene instance.