defineSlots es una macro del compilador (Vue 3.3+) que declara el contrato de tipos para los slots de tu componente. Sin ella, las props de los scoped slots no tienen tipo y los consumidores no obtienen autocompletado ni comprobación de tipos. Con ella, el template del padre sabe exactamente qué props expone cada slot.
Uso básico
<!-- DataList.vue -->
<script setup lang="ts">
interface Item {
id: number
label: string
}
defineProps<{ items: Item[] }>()
defineSlots<{
default(props: { item: Item; index: number }): any
empty(): any
}>()
</script>
<template>
<ul v-if="items.length">
<li v-for="(item, index) in items" :key="item.id">
<slot :item="item" :index="index" />
</li>
</ul>
<slot v-else name="empty" />
</template>Ahora cuando un padre usa este componente, las props del slot tienen tipo:
<DataList :items="products">
<!-- 'item' tiene tipo Item, 'index' tiene tipo number -->
<template #default="{ item, index }">
<span>{{ index + 1 }}. {{ item.label }}</span>
</template>
<template #empty>
<p>No items found.</p>
</template>
</DataList>Sintaxis
Cada slot es una firma de función donde el parámetro es el objeto de props del slot y el tipo de retorno es any:
defineSlots<{
slotName(props: { /* props del scoped slot */ }): any
}>()Para slots sin props de scope, usa una lista de parámetros vacía:
defineSlots<{
header(): any
default(): any
footer(): any
}>()Ejemplo real: componente Table
<!-- DataTable.vue -->
<script setup lang="ts" generic="T extends { id: string | number }">
defineProps<{
rows: T[]
columns: Array<{ key: keyof T; label: string }>
}>()
defineSlots<{
header(props: { columns: Array<{ key: keyof T; label: string }> }): any
cell(props: { row: T; column: keyof T; value: T[keyof T] }): any
empty(): any
}>()
</script>
<template>
<table>
<thead>
<slot name="header" :columns="columns">
<tr>
<th v-for="col in columns" :key="String(col.key)">{{ col.label }}</th>
</tr>
</slot>
</thead>
<tbody>
<template v-if="rows.length">
<tr v-for="row in rows" :key="row.id">
<td v-for="col in columns" :key="String(col.key)">
<slot name="cell" :row="row" :column="col.key" :value="row[col.key]">
{{ row[col.key] }}
</slot>
</td>
</tr>
</template>
<tr v-else>
<td :colspan="columns.length">
<slot name="empty">No data</slot>
</td>
</tr>
</tbody>
</table>
</template>Combinado con generic, las props del slot se infieren a partir del tipo de datos real pasado al componente.
Renderizado condicional de slots
Usa $slots para comprobar si se proporcionó un slot y evitar renderizar wrappers vacíos:
<script setup lang="ts">
defineSlots<{
header(): any
default(): any
footer(): any
}>()
</script>
<template>
<div class="card">
<header v-if="$slots.header" class="card-header">
<slot name="header" />
</header>
<div class="card-body">
<slot />
</div>
<footer v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</footer>
</div>
</template>Antes de defineSlots (Vue < 3.3)
Había que usar SlotsType en defineComponent:
import { defineComponent, type SlotsType } from 'vue'
export default defineComponent({
slots: Object as SlotsType<{
default: { item: Item; index: number }
empty: {}
}>,
setup(props, { slots }) {
// slots.default tiene tipo
}
})defineSlots reemplazó esto con una macro de una sola línea dentro de <script setup>.