Search code examples
typescriptvue.jsvuejs3vue-sfc

Recursive usage of slots in vue3/typescript/sfc


I'm trying to implement simple treeview using vue3 and typescript

npm init vue@latest // when asks about typescript support - say yes, and just press Enter on all other questions
cd '.\vue-project\'

add file TreeNode.vue

<template>
  <div>
    <div>
      <slot :item="item" />
    </div>
    <div style="margin-left: 10px" v-for="(child, index) in item.children" :key="index">
      <TreeNode :item="child">
        <template v-slot="{item}">
          <slot :item="item" />
        </template>
      </TreeNode>
    </div>
  </div>
</template>

<script setup lang="ts">  
  const props = defineProps<{ item: Item }>()
</script>

<script lang="ts">
  export type Item = {
    name: string
    children: Array<Item>
  }
</script>

when run npm run type-check

and got error:

src/components/TreeNode.vue:8:19 - error TS2615: Type of property 'default' circularly references itself in mapped type '{ [K in keyof (Readonly<InternalSlots> & { default: (_: any) => any; })]-?: (Readonly<InternalSlots> & { default: (_: any) => any; })[K] extends ((obj: infer O) => any) | undefined ? O : any; }'.

8         <template v-slot="{item}">

Any idea how to avoid this error ??


Solution

  • As I said in the comments, you code works perfectly fine (it renders). Error is from TS. My guess is it's some minor typing problem in Vue (or maybe a limitation of TS)

    I was able to get rid of the problem using dynamic slot names - more dynamic code makes it impossible to statically analyze the types, so TS stops complaining

    TreeNode.vue

    <script setup lang="ts">
    const props = defineProps<{ item: Item }>();
    </script>
    
    <script lang="ts">
    export type Item = {
      name: string;
      children?: Array<Item>;
    };
    
    const slotName = 'default';
    </script>
    
    <template>
      <div>
        <div>
          <slot :item="props.item" />
        </div>
        <div
          v-for="(child, index) in props.item.children"
          :key="index"
          style="margin-left: 10px"
        >
          <TreeNode :item="child">
            <template #[slotName]="{ item }: { item: Item }">
              <slot :item="item" />
            </template>
          </TreeNode>
        </div>
      </div>
    </template>
    

    Demo - just open another terminal and run npm run check