Subscribe on changes!

Generic Component Types does not work with InstanceType<typeof component>

avatar
May 19th 2023

Vue version

3.3.4

Link to minimal reproduction

https://stackblitz.com/edit/vitejs-vite-tpwnzy?file=src/App.vue

Steps to reproduce

Run "turbo typecheck" inside StackBlitz.

What is expected?

Either ref<InstanceType<typeof WithGeneric<number>>>() or ref<InstanceType<typeof WithGeneric>>() should generate correct typings.

What is actually happening?

src/App.vue:6:28 - error TS2344: Type 'typeof WithGeneric<number>' does not satisfy the constraint 'abstract new (...args: any) => any'.
  Type 'typeof WithGeneric<number>' provides no match for the signature 'new (...args: any): any'.

6 const a = ref<InstanceType<typeof WithGeneric<number>>>(); // type error
                             ~~~~~~~~~~~~~~~~~~~~~~~~~~

src/App.vue:7:28 - error TS2344: Type '<T>(__VLS_props: Options<T> & VNodeProps & AllowedComponentProps & ComponentCustomProps, __VLS_ctx?: Pick<{ props: Options<...>; expose(exposed: { ...; }): void; attrs: any; slots: {}; emit: any; }, "attrs" | ... 1 more ... | "slots"> | undefined, __VLS_setup?: { props: Options<...>; expose(exposed: { ...; }): void;...' does not satisfy the constraint 'abstract new (...args: any) => any'.
  Type '<T>(__VLS_props: Options<T> & VNodeProps & AllowedComponentProps & ComponentCustomProps, __VLS_ctx?: Pick<{ props: Options<...>; expose(exposed: { ...; }): void; attrs: any; slots: {}; emit: any; }, "attrs" | ... 1 more ... | "slots"> | undefined, __VLS_setup?: { ...; }) => VNode<...> & { ...; }' provides no match for the signature 'new (...args: any): any'.

7 const b = ref<InstanceType<typeof WithGeneric>>(); // type error
                             ~~~~~~~~~~~~~~~~~~

System Info

No response

Any additional comments?

No response

avatar
May 22nd 2023

I don't think it had anything to do with defineExpose

avatar
May 22nd 2023

Not directly at least, that's true. Most probably the typing is broken independant on how you define them.

avatar
May 25th 2023

Facing the same issue. Cant type my generic component with

const contentSwiperRef = ref<InstanceType<typeof ContentSwiper> | null>(null);

Bildschirmfoto 2023-05-25 um 15 30 40

avatar
May 27th 2023

Hey @ml1nk Can you try using ReturnType instead of InstanceType

Reference: https://github.com/vuejs/language-tools/issues/3206#issuecomment-1563399071

avatar
May 30th 2023

I tried it with ReturnType and ComponentExposed but neither one works.

avatar
Jun 5th 2023

To be more specific. ReturnType works for typing but resulting type is missing exposed functions

avatar
Jun 19th 2023

Same problem here:

const overviewTable = ref<InstanceType<typeof OverviewTable> | null>(null)

image

avatar
Jun 19th 2023

Just found out about this issues and maybe I got some workaround for now (feedback to verify welcome 🤞) For my use case this seems to work with props and defineExpose()

So for usage I landed on:

type TestType = { id: number; thing: string }
type MyGenericComponentType = PseudoComponent<typeof MyGenericComponent<TestType>>;
const myGenericComponentRef = ref<InstanceType<MyGenericComponentType>>();

and here's the definition until there is a fix (feel free to improve and adapt, it definitely needs some love)

import {
  DefineComponent,
  RendererElement,
  RendererNode,
  VNode,
  VNodeProps,
} from "vue";

type NoUndefined<T> = T extends undefined ? never : T;

type Props =
  | (VNodeProps & {
      [key: string]: any;
    })
  | null;

// maybe this can be typed as `SetupContext`
export type Context = {
  props: any;
  attrs: any;
  slots: any;
  emit: any;
  expose: (exposed?: any) => void;
};

export type ComponentReturn = VNode<RendererNode, RendererElement, Props> & {
  __ctx?: Context;
};

export type PseudoComponent<
  T extends (...args: any[]) => ComponentReturn,
  PseudoReturnType extends ComponentReturn = ReturnType<T>,
  PseudoContext extends ComponentReturn["__ctx"] = PseudoReturnType["__ctx"],
  PseudoProps = NoUndefined<PseudoContext>["props"],
  PseudoExposed = Parameters<NoUndefined<PseudoContext>["expose"]>[0]
> = DefineComponent<PseudoProps, PseudoExposed>;
avatar
Jun 21st 2023

Here's a plot twist:

The actual shape of type T defined by the generic component is known when this component's genericaly-typed prop is being assigned.

<template>
  <div><slot :item="item" /></div>
</template>

<script lang="ts" setup generic="T = any">
defineProps<{
  item: T
}>()

defineSlots<{
  default(props: { item: T }): any
}>()

defineExpose({
  sayHello(item: T) { console.log('Hello!', item) }
})
</script>

Assuming that happens inline, like so:

<template>
  <Example ref="example" :item="{ x: 1 }" v-slot="{ item }">
    {{ item }}
  </Example>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import Example from './Example.vue'

const example = ref<InstanceType<typeof Example>>()

onMounted(() => {
  example.value?.sayHello({ x: 2 })
})
</script>

I would expect that to work, but the types are totally wrong from that Example import of generic component.

The twist that I am referring to is in the binding to :item - that defines the shape of data of this particular instance of the Example component. So, if a ref is bound to a specific instance, we can immediately tell what the actual type is - by looking at what has been provided as the binding. And so the sayHello function can be fully typed - not relying on additional types provided to the ref<InstanceType<typeof Example<HERE>>().

If you try to inspect the content of that item in default slot it shows that item indeed has an x of type number:

image

Does that make sense?