core icon indicating copy to clipboard operation
core copied to clipboard

Dynamic slot using v-for with typescript implicitly has type 'any'

Open Moonlight63 opened this issue 4 years ago • 14 comments

Version

3.2.28

Reproduction link

github.com

Steps to reproduce

  • Clone the repo,

  • open folder in VSCode,

  • I have included a dev container that npm installs pnpm and vuenext and the volar extension,

  • once in dev container (or on host if you prefer) run pnpm install

  • Optionally run pnpm dev to start vite and open the page to see the rendered table.

What is expected?

For Typescript to not return an error.

What is actually happening?

Typescript is returning an error in (src/components/tabletest/Table.vue) line 84:

https://github.com/Moonlight63/temp-vue-typescript-bug/blob/e723dcd5905bb84f23dd67da3a15f3c465db4ae3/src/components/tabletest/Table.vue#L84

The error is:

'column' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

'column' is created by a v-for loop over a computed property of columns. The type should be inferred, and it normally is. This error only appears when I use any property of column to set the name of the slot dynamically.


I initially thought this was just an eslint error, so I opened an issue with the eslint vue plugin that has more info with pictures: https://github.com/vuejs/eslint-plugin-vue/issues/1773

Also worth noting. The component renders perfectly fine and everything works as expected, with the exception that in the app where I am actually using this functionality my build configuration won't allow the error. Because the error is occurring in the template, I can't just assert the type or otherwise tell typescript to ignore that line (that I know of).

Moonlight63 avatar Jan 24 '22 02:01 Moonlight63

After some further testing this behavior seems to be coming from the components definition file that is automatically generated as part of the vitesse template. This doesn't make it a vitesse specific issue as I was able to recreate the bug on a vue-cli project as well. It seems that when a vue file containing this pattern is added to a definition file like so:

declare module 'vue' {
  export interface GlobalComponents {
    Table: typeof import('./components/tabletest/Table.vue')['default']
  }
}

Typescript doesn't understand how to read the file. Unfortunately, I am only just learning how typescript works and am completely unqualified to know how to fix this problem beyond just removing and blacklisting that particular file from being in a typescript definition, which isn't exactly ideal. This pattern should work regardless of an entry in a definition file.

Moonlight63 avatar Jan 27 '22 12:01 Moonlight63

I'm also running into this, and have found a workaround. The problem occurs when there are both static and dynamic slot names. In this situation, the slots are incorrectly typed using only the static names.

To side-step the issue, we can define our static slots dynamically. One note about this, we have to use a template string to avoid typing the name as a string literal, see here for an example

<!-- before -->
<slot name="foobar" />

<!-- after -->
<slot :name="`${'foobar'}`" />

scottbedard avatar Mar 20 '22 23:03 scottbedard

Same issue happens when you try to pass couple of slots to a child component that uses dynamic slot names. No current workarounds so far

<!--
  Element implicitly has an 'any' type because
  expression of type 'string | number' can't be used to index type
-->
<template
  v-for="(_, slot) of $slots"
  #[slot]="data" <!-- error is here -->
>
  <slot
    :name="slot"
    :item="data?.item"
  />
</template>

the94air avatar Jun 14 '23 14:06 the94air

Wow, same issue here. Using a third-party component, which has all the slot names statically set.

I am looping over the slots to render them dynamically, and getting the same complaint from Typescript @the94air

Kasopej avatar Aug 15 '23 21:08 Kasopej

@Kasopej the main cause of this issue is that v-for will always attach the number type to the loop's index even though your main object/array type has a single index type that's usually a string. In this current case with $slots, it has a string index that doesn't overlap with the string | number type. I am not sure if this can be fixed without allowing typescript markup inside the dynamic v-slot somehow like so #[slot as keyof typeof $slots]="data" although that is not a valid syntax right now. I am not even sure if it is a good idea althought it is how you could workaround this in normal typescript. Now I can see that this issue might be a bit unrelated to the issue you're having. It maybe a good idea to (see if there is a similar issue to avoid duplicates or) open a new one.

the94air avatar Aug 16 '23 07:08 the94air

My case is a bit different from yours, but 'defineSlots' worked for me @the94air

// setup,
// only supported in 3.3+
defineSlots<{
  [key: string]: unknown;
}>();

seregarom avatar Aug 28 '23 18:08 seregarom

@seregarom that looks neat. I'll give it a try. Thanks for sharing!

the94air avatar Aug 28 '23 18:08 the94air

Vue 3.3.5 Still a problem and @seregarom workaround doesn't seem to work for me. Current workaround i found is asserting v-for source array as {}

    <template
        v-for="(name, index) of (Object.keys($slots) as {})"
        OR
        v-for="(_, name, index) in ($slots as {})"
      v-slot:[name]="scope"
      :key="index"
    >
      <slot :name="name" v-bind="{ scope }"></slot>
    </template>

Full error: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Readonly<{ message?: ((arg: VMessageSlot) => VNode<RendererNode, RendererElement, { [key: string]: any; }>[]) | undefined; clear?: (() => VNode<...>[]) | undefined; ... 8 more ...; counter?: ((arg: VCounterSlot) => VNode<...>[]) | undefined; }>'. No index signature with a parameter of type 'string' was found on type 'Readonly<{ message?: ((arg: VMessageSlot) => VNode<RendererNode, RendererElement, { [key: string]: any; }>[]) | undefined; clear?: (() => VNode<...>[]) | undefined; ... 8 more ...; counter?: ((arg: VCounterSlot) => VNode<...>[]) | undefined; }>'.ts(7053)

NojusM avatar Oct 26 '23 11:10 NojusM

The latest suggested proposal does not seems to do the trick on my side. I've been able to get rid of almost all of my 120 errors, two are remaining, due to.. dynamic slot names :) Any chance a month later?

kogratte avatar Nov 20 '23 22:11 kogratte

    <template
      v-for="(_, key) in $slots"
      v-slot:[key]="slotProps"
    >
      <slot
        v-if="slotProps"
        :name="key"
        v-bind="slotProps"
      />
      <slot
        v-else
        :name="key"
      />
    </template>

same error in typescript as @NojusM

mutongwu avatar Dec 22 '23 01:12 mutongwu

Not sure if this is related but also get an error when trying to compile this non-ts code.

[vite] Internal server error: Codegen node is missing for element/if/for node. Apply appropriate transforms first.

<template v-for="index in totalSlots">
    <template v-slot:[`title-${index}`]>
        <slot :name="`title-${index}`"></slot>
    </template>
    <template v-slot:[`content-${index}`]>
        <slot :name="`content-${index}`"></slot>
    </template>
</template>

mofman avatar Jan 05 '24 17:01 mofman

Vue 3.4

CleanShot 2024-02-23 at 11 14 34@2x

ReHelloWorld.vue

<script setup lang="ts">
import HelloWorld from './HelloWorld.vue'

interface Slots {
  default: (props: { default: string }) => string
  top: (props: { top: number }) => string
  bottom: (props: { bottom: boolean }) => string
}

defineSlots<Slots>()
</script>

<template>
  <HelloWorld>
    <template v-for="(_, name) in $slots as unknown as Readonly<Slots>" #[name]="slotProps">
      <slot :name="name" v-bind="slotProps" />
    </template>
  </HelloWorld>
</template>

It looks fine but I have a type error.

Argument of type '{ default: string; } | { top: number; } | { bottom: boolean; }' is not assignable to parameter of type '{ default: string; } & { top: number; } & { bottom: boolean; }'.
  Type '{ default: string; }' is not assignable to type '{ default: string; } & { top: number; } & { bottom: boolean; }'.ts(2345)

hungify2022 avatar Feb 23 '24 04:02 hungify2022

Thanks @NojusM your snippet resolved the issue I was having.

inkbeard avatar Mar 09 '24 15:03 inkbeard

How to fix it in current project?

itelmenko avatar Mar 13 '24 15:03 itelmenko

with a combination of <!-- @vue-expect-error -->, <!-- @vue-skip -->, and using defineSlots on both of my parent and child components I managed to get the types and ignore the errors

jd1378 avatar May 31 '24 05:05 jd1378

Getting a similar error with

vue: 3.5.3
vue-tsc: 2.1.6

A simplified version of my components follow:

<!-- Table.vue -->
<template>
  <table>    
    <tbody>
      <tr v-for="(item, index) of items" :key="item">
        <td v-for="column of columns" :key="column.id" >
          <slot :name="column.id" :item="item" :index="index" />
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script setup lang="ts">
export interface Column {
  id: string;
  title: string;
}

interface IProps {
  columns: Column[];
  items: any;
}

defineProps<IProps>();
</script>
<!-- PlayerTable.vue -->

<Table :columns="columns" :items="rounds">  
  <template v-for="player of players" v-slot:[player.id]="slotProp" :key="`${slotProp.item.id}-${player.id}`">
    <SomeComponent :info="slotProp.info[player.id]" />
  </template>
</Table>
</template>

<script setup lang="ts">
// ... imports and etc

const rounds = computed(() =>[
  { round: 1, info: { player1: "foo", player2: "bar" }}
]);

const columns = computed<Column[]>(() => {
  return [
    {
      id: "round",
      title: "Round",
    },
    ...players.value.map((player) => {
      return {
        id: player.id,
        title: player.name,
      };
    }),
  ];
});
</script>

The error I get is

Property 'slotProp' does not exist on type 'CreateComponentPublicInstanceWithMixins<ToResolvedProps<IProps, {}>, { Table: typeof Table; PlayerTable: typeof PlayerTable; PlayerIcon: typeof PlayerIcon; players: typeof players; rounds: typeof rounds; columns: typeof columns; }, ... 23 more ..., {}>'.

17     <template v-for="player of players" v-slot:[player.id]="slotProp" :key="`${slotProp.item.id}-${player.id}`">

Hanawa02 avatar Sep 09 '24 16:09 Hanawa02

As @NojusM suggested, the following does the trick:

    <template
      v-for="(_, name) in ($slots as {})"
      :key="name"
      #[name]
    >
      <slot :name />
    </template>

Cast $slots as {}.

inlinecoder avatar Sep 10 '24 02:09 inlinecoder

It doesn't work in my case with vitepress. Even I put $slots as {} It's still not working and shows the same error

Note: This only happens if I upgrade vue-tsc to 2.2.0. This issue does not happen in 2.1.10

<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
</script>

<template>
  <DefaultTheme.Layout>
    <template v-for="(_, slot) in $slots" #[slot]="scope">
      <!--               ^^^^ slot implicitly has type any because it does not have a type annotation and is referenced directly or indirectly in its own initializer. -->
      <slot :name="slot" v-bind="scope" />
    </template>
  </DefaultTheme.Layout>
</template>

xsjcTony avatar Jan 23 '25 04:01 xsjcTony

    @xsjcTony  this is working: 
           <!--suppress JSUnusedLocalSymbols -->
            <template v-for="(_, slot) of $slots as {}" #[slot]="scope">
                <slot :name="slot" v-bind="(scope ?? {}) as Record<string, any>" />
            </template>

stephane303 avatar Jan 23 '25 09:01 stephane303

        <template v-for="(_, slot) of $slots as {}" #[slot]="scope">
            <slot :name="slot" v-bind="(scope ?? {}) as Record<string, any>" />
        </template>

@stephane303 For me doesn't work.

"vue-tsc": "^2.2.0".
"typescript": "5.7.2",
"vue": "^3.5.13",

Image

ricardo17coelho avatar Jan 27 '25 06:01 ricardo17coelho

You can use <!-- @vue-skip --> to get rid of the error meanwhile this is fixed

stephane303 avatar Feb 05 '25 12:02 stephane303

Workaround so far:

<script setup lang="ts">
import { useSlots } from 'vue'

const slots = useSlots() as any
</script>

<template>
  <div>
    <template v-for="(_, name) in slots" #[name]="slotProps">
      <slot :name="name" v-bind="slotProps || {}"></slot>
    </template>
  </div>
</template>

pegiadise avatar Feb 13 '25 17:02 pegiadise

The Vue Language Tools docs explicitly talk about this problem and suggest using defineSlots(); which works nicely for us.

<template>
  <template v-for="(_, name) in $slots" #[name]="data">
    <slot :name="name" v-bind="data" />
  </template>
</template>

<script setup lang="ts">
// see https://vuejs-language-tools.vercel.app/features/slots#how-to-handle-indeterminate-slot-types
defineSlots();
</script>

kkuegler avatar Feb 13 '25 18:02 kkuegler

Workaround so far:

This works for me.

ricardo17coelho avatar Feb 13 '25 18:02 ricardo17coelho

The Vue Language Tools docs explicitly talk about this problem and suggest using defineSlots(); which works nicely for us.

this works for me too!

ricardo17coelho avatar Feb 13 '25 18:02 ricardo17coelho

The Vue Language Tools docs explicitly talk about this problem and suggest using defineSlots(); which works nicely for us.

I never know there's such website😂. This is indeed the solution. Hope I can push this to the very top but I'm not able to.

xsjcTony avatar Feb 13 '25 23:02 xsjcTony

Any news regarding this issue?

denys119 avatar Feb 18 '25 18:02 denys119

Thanks @kkuegler

edison1105 avatar Feb 24 '25 09:02 edison1105

Why was this Issue closed ? The problem is still there with no fix.

AzizFacilex avatar Feb 26 '25 14:02 AzizFacilex

Why was this Issue closed ? The problem is still there with no fix.

see https://github.com/vuejs/core/issues/5312#issuecomment-2657383224 Sorry, Does this solve your problem?

edison1105 avatar Feb 26 '25 14:02 edison1105