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

This expression is not callable. Type '{}' has no call signatures - Dynamic slots + Generic #3141

Closed
jd-solanki opened this issue May 7, 2023 · 27 comments
Assignees
Labels
good reproduction ✨ This issue provides a good reproduction, we will be able to investigate it first question Further information is requested

Comments

@jd-solanki
Copy link
Contributor

Hi 👋🏻

I was playing with the generic components feature and stumbled upon this issue where I get a type error on the slot like below:
image

Reproduction: https://github.com/jd-solanki/volar-vue-playground/blob/generic-component-type/src/bar/Bar.vue

Note
Watch out for branch

  System:
    OS: Windows 10 10.0.22621
    CPU: (16) x64 AMD Ryzen 7 3700X 8-Core Processor
    Memory: 6.86 GB / 15.95 GB
  Binaries:
    Node: 18.14.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - C:\Program Files\nodejs\yarn.CMD
    npm: 9.4.1 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.22621.1555.0), Chromium (112.0.1722.68)
    Internet Explorer: 11.0.22621.1
  npmPackages:
    vue: 3.3.0-beta.4 => 3.3.0-beta.4 
    vue-tsc: ^1.6.4 => 1.6.4

P.S. It will be much easier for you if you add issue template

@jd-solanki
Copy link
Contributor Author

Upon further inspection, I found that component meta shown empty slots array:
image

I guess vue component meta doesn't properly handle the generics yet 🤔

@jd-solanki jd-solanki changed the title When using dynamic slot with static slot error occurs: This expression is not callable. Type '{}' has no call signatures This expression is not callable. Type '{}' has no call signatures - Dynamic slots + Generic May 9, 2023
@jd-solanki
Copy link
Contributor Author

@johnsoncodehk is this supported by volar ATM?

Because I think both are the same thing 🤔

I also tried doing something like this for my actual code but doesn't seems to work:

{ [name in `col-${RowKey<Row>}`]: (_: { row: Row; colIndex: number }) => any }

@johnsoncodehk
Copy link
Member

This doesn't seem to be a Volar issue, you can reproduce it in script.

<script lang="ts" setup generic="Row extends Record<string, unknown>">
import { objectKeys } from '../utils';
import { aTableProps, aTableSlots } from './meta';

const props = defineProps(aTableProps<Row>())

const _slots = aTableSlots<Row>(
    objectKeys(props.rows[0] || {}),
)
defineSlots<typeof _slots>()

for (const col of props.cols) {
    _slots[`header-${col.name}`]?.({ col }); // Type '{}' has no call signatures.ts(2349)
}
</script>

@jd-solanki
Copy link
Contributor Author

Hi @johnsoncodehk

Thanks for your inspection. May I know how to resolve this because according to me I'm generating correct object in aTableSlots?

Awaiting your response.

@jd-solanki
Copy link
Contributor Author

Strange thing is if you comment before-table slot then type error went away.

Extending my previous comment, I guess I'm generating correct object but it looks like there's issue with the generated type.

@johnsoncodehk johnsoncodehk added question Further information is requested good reproduction ✨ This issue provides a good reproduction, we will be able to investigate it first labels May 17, 2023
@xiaoxiangmoe
Copy link
Collaborator

@jd-solanki Hi, it seems to be this is a TypeScript's limit:

type Slots<Foo extends string> = {
  readonly "before-table": (_: any) => null;
} & Record<`header-${Foo}`, (_: { col: any }) => any>;

function foo<Foo extends string>(
  slots: Slots<Foo>,
  key: `header-${Foo}`
) {
  const slot = slots[key];
  // @ts-expect-error This expression is not callable. Type '{}' has no call signatures.ts(2349)
  slot?.({ col: 1 });
}

If you simplify your code:

type Slots<Foo extends string> = Record<`header-${Foo}`, (_: { col: any }) => any>;

function foo<Foo extends string>(
  slots: Slots<Foo>,
  key: `header-${Foo}`
) {
  const slot = slots[key];
  slot?.({ col: 1 });
}

All works well.

Maybe you should create a issue to TypeScript.

@xiaoxiangmoe
Copy link
Collaborator

Closed this issue because it is an upstream issue, if you need any other help, or if you find any other volar issues, please feel free to raise them below.

@jd-solanki
Copy link
Contributor Author

Might be similar: vuejs/core#8352

@xiaoxiangmoe
Copy link
Collaborator

Can you plese share your thoughts on this

I'm not quite sure what's going on, but I found that in your latest example, the slots and key are controlled by the same generic Args.

And in your original example, actually Rows and key are changed independently.
For this reason, I extracted a simplified demo and used a function foo to demonstrate the origin of your error Type '{}' has no call signatures.ts.

@jd-solanki
Copy link
Contributor Author

jd-solanki commented May 19, 2023

Can we improve something at volar side considering this kind of common usage? vuejs/core#7982 (comment) vuejs/core#8352

What's your thoughts on reopening and renaming this issue?

@xiaoxiangmoe
Copy link
Collaborator

@jd-solanki
Copy link
Contributor Author

jd-solanki commented May 19, 2023

Hi @xiaoxiangmoe

I tried doing that and it still gives Type '{}' has no call signatures.. However, If I remove 'before-table': (_: any) => null slot definition just like in my reproduction it works.

// meta.ts
export function aTableSlots<Row extends Record<string, unknown>>(colKeys: RowKey<Row>[]) {
    return ({
    // If I comment below line then error in SFC is gone 🤔
    'before-table': (_: any) => null,
    
    ...colKeys.reduce((a, colKey) => (
      { ...a, [`header-${colKey}`]: Function }
    ), {} as { [K in `${RowKey<Row>}-cell`]: (props: { item: RowKey<Row>; }) => any; }),
  }) as const
}
<!-- script part is same -->
<template>
    <div class="wrapper">
        <div v-for="row in props.rows">
        <header v-for="(col, index) in props.cols" :key="index">
            <slot :name="`${col.name}-cell`" :col="col"></slot>
            <!--  This expression is not callable. Type '{}' has no call signatures. -->           
        </header>
        </div>
    </div>
</template>

In my reproduction and this example, if you comment out 'before-table': (_: any) => null, no error will get thrown ✨

@jd-solanki
Copy link
Contributor Author

jd-solanki commented May 19, 2023

Here's video demo:

Screen.Recording.2023-05-19.at.3.26.12.PM.mov

@xiaoxiangmoe
Copy link
Collaborator

Hi, @jd-solanki.

In your example:
try:

update vue to 3.3.4

change function aTableSlots

export function aTableSlots<Row extends Record<string, unknown>>(colKeys: RowKey<Row>[]) {
    return ({
    // If I comment below line then error in SFC is gone 🤔
    'before-table': (_: any) => null,
    
    ...colKeys.reduce((a, colKey) => (
      { ...a, [`header-${colKey}`]: Function }
    ), {} as Record<`header-${RowKey<Row>}`, (_: { col: ATablePropColumn<Row> }) => any>),
  }) as const
}

to

export function aTableSlots<Row extends Record<string, unknown>>(colKeys: RowKey<Row>[]) {
    return ({
    // If I comment below line then error in SFC is gone 🤔
    'before-table': (_: any) => null,
    
    ...colKeys.reduce((a, colKey) => (
      { ...a, [`header-${colKey}`]: Function }
    ), {} as { [K in keyof Row as `header-${K & string}`]:  (_: { col: ATablePropColumn<Row> }) => any } ),
  }) as const
}

And then change

            <slot :name="`header-${col.name}`" :col="col"></slot>

to

            <slot :name="`header-${String(col.name)}`" :col="col"></slot>

Can you see weather it fix your problem?

@pikax
Copy link
Member

pikax commented May 19, 2023

I would strongly recommend not using aTableSlots since the return type is not used!

I would recommend switching to a type

type aTableSlots<T extends Record<string, unknown>> = {
  [K in keyof T as `header-${K & string}`]: (_: {
    col: ATablePropColumn<T>
  }) => any
} & {
  ['before-table']?: (_: { a: string }) => any
}

The reason being defineSlots is plainly a typescript implementation without any runtime validation.

To use this new aTableSlots is fairly easy by:

const slots = defineSlots<aTableSlots<Row>>()

full example:

<script lang="ts" setup generic="Row extends { [K: string]: any}">
interface ATablePropColumn<Row extends Record<string, unknown>> {
  name: RowKey<Row>
}
const props = defineProps<{
  rows: Row[]
  cols: ATablePropColumn<Row>[]
}>()

type RowKey<Row extends Record<string, unknown>> = keyof Row & string

type aTableSlots<T extends Record<string, unknown>> = {
  [K in keyof T as `header-${K & string}`]: (_: {
    col: ATablePropColumn<T>
  }) => any
} & {
  ['before-table']?: (_: { a: string }) => any
}

const slots = defineSlots<aTableSlots<Row>>()

for (const col of props.cols) {
  const slot = slots[`header-${String(col.name)}`]
  slot?.({ col })
}
</script>

<template>
  <div class="wrapper">
    <div v-for="row in rows">
      <header v-for="(col, index) in cols" :key="index">
        <slot :name="`header-${String(col.name)}`" :col="col"></slot>
        <!--  This expression is not callable. Type '{}' has no call signatures. -->
      </header>
    </div>
  </div>
</template>

@xiaoxiangmoe
Copy link
Collaborator

@pikax I found your demo and @jd-solanki 's demo also works without remove Readonly<> in vue3.3.4.

@pikax
Copy link
Member

pikax commented May 19, 2023

@xiaoxiangmoe

I get an error on vue 3.3.4 🤔
image

import { defineProps, defineSlots } from "vue";

type RowKey<Row extends Record<string, unknown>> = keyof Row & string;
interface ATablePropColumn<Row extends Record<string, unknown>> {
  name: RowKey<Row>;
}

function fakeComponent<Row extends { [K: string]: any }>() {
  const props = defineProps<{
    rows: Row[];
    cols: ATablePropColumn<Row>[];
  }>();

  type aTableSlots<T extends Record<string, unknown>> = {
    [K in keyof T as `header-${K & string}`]: (_: { col: ATablePropColumn<T> }) => any;
  } & {
    ["before-table"]?: (_: { a: string }) => any;
  };

  const slots = defineSlots<aTableSlots<Row>>();

  for (const col of props.cols) {
    const slot = slots[`header-${String(col.name)}`];

    slot?.({ col });
  }
}

fakeComponent();

@xiaoxiangmoe
Copy link
Collaborator

Oh, you are right.

@jd-solanki
Copy link
Contributor Author

jd-solanki commented May 20, 2023

Old response

Hey @pikax and I'll try to go with full TS like below:

import { OptionalKeysOf } from 'type-fest'

export interface AlertProps {
  rows: unknown[],
  icon?: string
  appendIcon?: string
  dismissible?: boolean
  modelValue?: boolean
}

export const alertPropsDefault = {
  appendIcon: 'close',
} satisfies Pick<AlertProps, OptionalKeysOf<AlertProps>>

(above is just playground component and file is meta.ts)

Before refactoring the codebase 4th time I just want to comfirm that,

  1. Is this fine?
  2. I'll have meta.ts and <Component-SFC>.vue file
  3. I'll export shared component props from meta.ts and reuse them.

Final out of context questions:
Can I somehow export and reuse (might need all of or few of them) props from SFC if I go with withDefaults(defineProps<Props>(), { ... }

Thanks.

I'm using runtime code (JS) because I have to loop over object to pass the slots to the child like this.
Another thing to note is I also have to pass this slots to the appropriate location like this

@jd-solanki
Copy link
Contributor Author

@xiaoxiangmoe Can we remove upstream label?

@xiaoxiangmoe
Copy link
Collaborator

@jd-solanki

Can you see weather it fix your problem? #3141 (comment)

Is there any bug should fix in volar?

@jd-solanki
Copy link
Contributor Author

Hi @xiaoxiangmoe I was busy with some other stuff. I'll let you know once soon.

@jd-solanki
Copy link
Contributor Author

Hi @xiaoxiangmoe 👋🏻

It works 💚

I wanted to know is this some kind of hack to workaround volar or TS limitation.

I'm also creating new component based on this component and passing slots down to this component. Will this create any troublem because I'm having type issue in another component that uses it but I'm unable to identify if it's from volar or my side.

Thanks.

@xiaoxiangmoe
Copy link
Collaborator

This is not hack to workaround volar, it just some workaround for TypeScript.

@jd-solanki
Copy link
Contributor Author

I wonder performing that change will make type as header-${string} => string for slot name instead of actual names like header-name? Is it true?

Are you interested in checking the another component (build on top of mentioned component) error after perfoming mentioned changes (that solves the type error for that component though)?

Regards.

@so1ve
Copy link
Member

so1ve commented Dec 13, 2023

Fixed by vuejs/core#8374

@so1ve so1ve closed this as completed Dec 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good reproduction ✨ This issue provides a good reproduction, we will be able to investigate it first question Further information is requested
Projects
None yet
Development

No branches or pull requests

5 participants