Vue3: How to type a component instance template ref with exposed methods?

10.1k views Asked by At

I have this component A with an exposed method send

A.vue:

<script setup lang="ts">
function send(data: string) {
  console.log(data)
}

defineExpose({ send })
</script>

And the component B which imports this component

B.vue

<template>
  <ComponentA ref="a" />
</template>
<script setup lang="ts">
import ComponentA from '@/component-a.vue'

const a = ref()

onMounted(() => a.value.send(22)) // should be a type error
</script>

How do I type the imported component, so that when I call its methods through refs, it checks exposed method names and types of passed arguments?

I tried what I could google: ComponentPublicInstance<typeof ComponentA> but it doesn't seem to check the methods.

EDIT:

here's shims-vue.d.ts (as generated by vue-cli):

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

EDIT:

Here's the reproduction.

As you can see, in HelloWorld.vue, a is ref to an instance of ComponentA. I attempted to type it with const a = ref<InstanceType <typeof ComponentA>>(), but no luck, it gets typed as any

screenshot from vscode

2

There are 2 answers

1
Jeff Bowman On

As far as TypeScript is concerned, ComponentA is of type DefineComponent<{}, {}, any>, because your shims-vue.d.ts declares it as such. (I imagine you'll have more luck with import ComponentA as opposed to import { ComponentA }, too.) TypeScript does not understand how to parse Vue SFC files, so TypeScript is forced to trust your .d.ts file.

In IDEs like VSCode using extensions like Volar or Vetur, you might get more specific type checking than TypeScript alone will get you; that's because Vetur and Volar are smart enough to read Vue SFC files and type them appropriately, disregarding the global .d.ts shim.

If you want smarter TypeScript checking without using an IDE, then as on the Vue Typescript Overview section you'll need to use vue-tsc, which is a TypeScript wrapper that shares code with Volar but understands SFC files. You might can also use vue-tsc to generate a more-specific .d.ts file, which would write out the context TypeScript needs to validate your SFCs without using the Vue-aware wrapper.

5
tony19 On

The instance type of ref() is a Ref<T>, where T (defaults to any) specifies the type of its .value property.

You didn't provide the generic type, and no initializer argument exists to infer the type from, so TypeScript assumes T is any, and thus a.value has a type of any. Therefore, a.value.send is also of type any, which would not provide any type safety (as you had observed).

In this case, you should declare the generic type of ref with:

                       
const a = ref<InstanceType<typeof ComponentA>>()
a.value?.send(22) // error: Argument of type 'number' is not assignable to parameter of type 'string'.

screenshot of TypeScript error in action