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

2

8

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

Cabinetwork answered 12/4, 2022 at 18:19 Comment(0)
K
16

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

Kurman answered 12/4, 2022 at 21:8 Comment(5)
I tried InstanceType, but for me it results in any type. From your screenshot I can tell that for you it's not the case. Did you explicitly define ComonentA's type somwhere? – Cabinetwork
I cannot reproduce the issue with your repo (screenshot). Are you using Volar? – Kurman
I tried it both with volar and vetur – Cabinetwork
Hmm. I cannot reproduce it on macOS Big Sur. What OS are you using? Do you have both Vetur and Volar enabled at the same time? If so, disable Vetur. And do you have TypeScript Vue Plugin for Volar installed? – Kurman
@Cabinetwork FYI, not sure if this could be the problem, but try enabling Volar's takeover mode, as described in docs. – Kurman
H
3

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.

Hereat answered 12/4, 2022 at 19:21 Comment(1)
I'm using vetur and in SFC file I can get correct type from InstanceType<typeof SomeComponent> , but in .ts file I just get any, any solutions to let ts file get correct type? – Visceral

© 2022 - 2024 β€” McMap. All rights reserved.