Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using defineSlots with dynamic slot names causes error TS2349: This expression is not callable. #2758

Closed
mtorromeo opened this issue Apr 28, 2023 · 4 comments

Comments

@mtorromeo
Copy link

mtorromeo commented Apr 28, 2023

I have a generic component, where the slots are defined by the keys of the object bound to the generic parameter.

This is a simplified version of such a component:

MyComp.vue

<template>
  <div>
    <slot />
    <template v-for="(value, key) of itemData">
      <slot
        v-if="typeof key === 'string'"
        :name="`cell:${key}`"
        :value="value"
      />
    </template>
  </div>
</template>

<script lang="ts" setup generic="T extends DataType">
export type DataType = Record<string, string>;

defineSlots<
  {
    [K in keyof T as K extends string ? `cell:${K}` : never]: { value: T[K] };
  } & {
    default: Record<string, any>;
  }
>();

defineProps<{
  itemData: T;
}>();
</script>

And it can be used like this:

App.vue

<template>
  <MyComp :item-data="{ a: 'A', b: 'B' }">
    <template #cell:a="{ value }">
      <h1>{{ value }}</h1>
    </template>

    <template #cell:b="{ value }">
      <h2>{{ value }}</h2>
    </template>

    test
  </MyComp>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import MyComp from './components/MyComp.vue';

export default defineComponent({
  components: {
    MyComp: MyComp<{ a: string; b: string; }>,
  },
});
</script>

Up to version 1.2 the typechecking in App.vue was working fine, the slots names were recognized and so was the type of the scope variables.

Since the vue-tsc update that validates the slots in the component that defines them, I am now getting the following errors:

src/components/MyComp.vue:3:5 - error TS2349: This expression is not callable.
  Not all constituents of type 'Record<string, any> | ((props: ({ [K in keyof T as K extends string ? `cell:${K}` : never]: { value: T[K]; }; } & { default: Record<string, any>; })["default"]) => any)' are callable.
    Type 'Record<string, any>' has no call signatures.

3     <slot />
      ~~~~~~~~

src/components/MyComp.vue:5:7 - error TS2349: This expression is not callable.
  Type '{}' has no call signatures.

  5       <slot
          ~~~~~
  6         v-if="typeof key === 'string'"
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
... 
  8         :value="value"
    ~~~~~~~~~~~~~~~~~~~~~~
  9       />
    ~~~~~~~~

src/App.vue:21:13 - error TS2769: No overload matches this call.
  The last overload gave the following error.
    Type '(__VLS_props: { itemData: { a: string; b: string; }; } & VNodeProps & AllowedComponentProps & ComponentCustomProps, __VLS_ctx?: Pick<...> | undefined, __VLS_setup?: { ...; }) => VNode<...> & { ...; }' is not assignable to type 'Component<any, any, any, ComputedOptions, MethodOptions>'.
      Type '(__VLS_props: { itemData: { a: string; b: string; }; } & VNodeProps & AllowedComponentProps & ComponentCustomProps, __VLS_ctx?: Pick<...> | undefined, __VLS_setup?: { ...; }) => VNode<...> & { ...; }' is not assignable to type 'FunctionalComponent<any, any>'.
        Types of parameters '__VLS_ctx' and 'ctx' are incompatible.
          Type 'Omit<{ attrs: Data; slots: Readonly<InternalSlots>; emit: ((event: unknown, ...args: any[]) => void) | ((event: string, ...args: any[]) => void); expose: (exposed?: Record<string, any> | undefined) => void; }, "expose">' is not assignable to type 'Pick<{ props: { itemData: { a: string; b: string; }; }; expose(exposed: {}): void; attrs: any; slots: __VLS_DefineSlots<{ "cell:a": { value: string; }; "cell:b": { value: string; }; } & { default: Record<string, any>; }>; emit: any; }, "attrs" | ... 1 more ... | "slots">'.
            Types of property 'slots' are incompatible.
              Type 'Readonly<InternalSlots>' is missing the following properties from type '__VLS_DefineSlots<{ "cell:a": { value: string; }; "cell:b": { value: string; }; } & { default: Record<string, any>; }>': "cell:a", "cell:b", default

21     MyComp: MyComp<{ a: string; b: string; }>,
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  src/App.vue:21:13
    21     MyComp: MyComp<{ a: string; b: string; }>,
                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Did you mean to call this expression?
  node_modules/.pnpm/@vue+runtime-core@3.2.47/node_modules/@vue/runtime-core/dist/runtime-core.d.ts:666:25
    666 export declare function defineComponent<PropsOptions extends Readonly<ComponentPropsOptions>, RawBindings, D, C extends ComputedOptions = {}, M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = {}, EE extends string = string, I extends ComponentInjectOptions = {}, II extends string = string>(options: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE, I, II>): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>;
                                ~~~~~~~~~~~~~~~
    The last overload is declared here.

Reproduction: Stackblitz

@johnsoncodehk
Copy link
Member

johnsoncodehk commented May 1, 2023

There were multiple problems in this use case, some fixed by Volar:

Others need to wait for upstream fixes, or changes to your code.

  1. You should access itemData via props.
- <template v-for="(value, key) of itemData">
+ <template v-for="(value, key) of props.itemData">
- defineProps<{
+ const props = defineProps<{
  itemData: T;
}>();

Upstream issue: vuejs/core#8144

  1. The slots key type definition needs to be simplified.
- [K in keyof T as K extends string ? `cell:${K}` : never]: { value: T[K] };
+ [K in `cell:${string}`]: { value: T[keyof T] };

@mtorromeo
Copy link
Author

mtorromeo commented May 2, 2023

Glad the comments have been reopened so that I can reply again!

The slots key type definition needs to be simplified.

- [K in keyof T as K extends string ? `cell:${K}` : never]: { value: T[K] };
+ [K in `cell:${string}`]: { value: T[keyof T] };

@johnsoncodehk
Thanks for providing an alternative solution, but this is not ideal. The suggested slot definition will fail to recognize invalid slot names and mix up the types of the context variables.

E.g.:
If I use the component as MyComp<{ a: string; b: number; }>, and then try to use a slot like <template #cell:c> typescript will not notify me about the wrong slot name.
Also, when I use <template #cell:a="{ value }"> typescript will infer the type string | number for value, while with the original version of the code value would have the correct type string.

This is a regression from version 1.2, and I hope it can be supported again.

@johnsoncodehk
Copy link
Member

This is due to the limitation caused by supporting short definitions of slots, we removed it in #3116, you need to modify your component code accordingly, FYI this is the complete modified code:

<template>
  <div>
    <slot />
    <template v-for="(value, key) of props.itemData">
      <slot
        v-if="typeof key === 'string'"
        :name="`cell:${key}`"
        :value="(value as T[string])"
      />
    </template>
  </div>
</template>

<script lang="ts" setup generic="T extends DataType">
export type DataType = Record<string, string>;

defineSlots<
  {
    [K in keyof T as K extends string ? `cell:${K}` : never]?: (_: { value: T[K] }) => any;
  } & {
    default?: (_: Record<string, any>) => any;
  }
>();

const props = defineProps<{
  itemData: T;
}>();
</script>

@VladimirCores
Copy link

But how could I preserve the order of items from input object if I want 'a' come after 'b'?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants