with TypeScript: defineSlots
A scoped slot carries data from a child component back into a parent's template, yet without explicit typing TypeScript long treated that data as plain any. A typo like item.naem instead of item.name would only surface in the browser, never at compile time. defineSlots
Table of Contents
- 1. The problem with untyped slots: a long-standing type gap
- 2. defineSlots
(): basics and syntax - 3. Typing scoped slot props: a practical example
- 4. IDE autocompletion in the parent's template
- 5. Optional slots: pitfalls with optional entries and v-if checks
- 6. Typing slots without scoped props: pure content slots
- 7. Typing multiple named slots at once
- 8. Typed versus untyped slots in practice
- 9. Common mistakes and conclusion
- 10. Summary
- 11. FAQ
1. The problem with untyped slots: a long-standing type gap
While props have long been fully typeable through defineProps
This gap was especially frustrating because scoped slots are frequently used exactly where individual, component-specific rendering is needed, for instance in list or table components where the parent defines its own markup per row. Type safety would have delivered particular value right there, since a typo in a property name would otherwise only surface through manual testing in the browser instead of being flagged red in the editor.
2. defineSlots(): basics and syntax
Since Vue 3.3, defineSlots
Similar to defineProps and defineEmits, this is a pure compile-time construct that generates no additional code at runtime and performs no runtime validation. The entire effect of defineSlots plays out exclusively within TypeScript's type checking, both inside the component itself and, crucially, in the template of every component that later consumes it.
<script setup lang="ts">
interface Product {
id: string
name: string
price: number
}
defineProps<{ items: Product[] }>()
defineSlots<{
default(props: { item: Product; index: number }): void
}>()
</script>
<template>
<ul>
<li v-for="(item, index) in items" :key="item.id">
<slot :item="item" :index="index" />
</li>
</ul>
</template>
3. Typing scoped slot props: a practical example
In practice it is worth extracting the type of the scoped slot props into a standalone interface or type alias once several slots share the same data shape or the type becomes complex enough to clutter the defineSlots declaration itself. For a product list, you would first define a type ProductSlotProps that is then reused both in defineSlots and, where needed, elsewhere inside the component.
It is important that the data actually passed in the
<script setup lang="ts">
interface Product {
id: string
name: string
price: number
}
interface ProductSlotProps {
item: Product
index: number
isLast: boolean
}
defineProps<{ items: Product[] }>()
defineSlots<{
default(props: ProductSlotProps): void
}>()
</script>
<template>
<ul>
<li v-for="(item, index) in items" :key="item.id">
<slot :item="item" :index="index" :is-last="index === items.length - 1" />
</li>
</ul>
</template>
4. IDE autocompletion in the parent's template
The real value of defineSlots does not show up in the child component itself but in the template of every component that consumes the typed slot. Volar reads the defineSlots declaration and infers the type of the destructuring inside , so the IDE offers full autocompletion for item.name or item.price, complete with type information on hover.
A typo like item.naem is flagged immediately as a red error in the editor, long before the application is even started, which in larger projects with many reused list and table components makes the difference between a silent bug in production and an immediately visible compile error. This type checking works equally reliably for both named slots and the default slot.
5. Optional slots: pitfalls with optional entries and v-if checks
Not every slot needs to actually be filled by every consuming component, for instance an optional footer slot on a card component that is only sometimes used. In defineSlots, such a slot is marked optional with a question mark after its name, so footer?(props: FooterSlotProps): void, letting TypeScript know the slot may be absent in the consuming component without reporting an error.
A common pitfall here is checking v-if="$slots.footer" inside the child component's template to determine whether the slot was actually filled before rendering the surrounding wrapper, for instance to avoid unnecessary empty markup. This check works purely at runtime through the $slots object, independent of TypeScript typing, and needs to be maintained in addition to the optional type declaration in defineSlots, since defineSlots describes only the type layer and does not automatically generate a runtime check for whether an optional slot was actually used.
6. Typing slots without scoped props: pure content slots
Not every slot carries data back from the child component; many slots simply act as placeholders for arbitrary, static markup that the parent inserts without the child supplying any data for it, such as a simple header slot on a card component. For these pure content slots, defineSlots just declares an empty props object, for example header(props: {}): void, or, when no slot props exist at all, directly header(): void.
Even for these simple cases, an explicit declaration in defineSlots pays off, because it additionally documents which slots a component offers at all, regardless of whether they carry data. For consuming components, Volar then offers autocompletion for the available slot names themselves even for pure content slots, for instance when typing #head in the editor, which noticeably improves discoverability for components with many named slots.
7. Typing multiple named slots at once
A typical card or layout component often offers several named slots at once, such as header, default and footer, each potentially carrying a different set of scoped slot props. defineSlots accepts an object with multiple entries at once for this, where each key represents a slot name and each value can be typed independently of the others, with no slots influencing each other.
This structure makes defineSlots especially valuable for layout components with a complex slot architecture, because a single, central type declaration at the top of the component immediately documents which slots exist at all, which of them carry scoped slot props, and which are optional. For new team members, this declaration in practice often replaces separate documentation of the component's API, since it lives directly in the code and stays automatically up to date.
8. Typed versus untyped slots in practice
Without defineSlots, scoped slots continue to work flawlessly at runtime; Vue needs no type declaration to correctly pass data into a slot. The entire difference plays out exclusively at the level of static type checking: without defineSlots, slot props are implicitly any, the editor offers no autocompletion, and typos in property names only surface at runtime, if at all, because a missing property in the template simply renders as undefined with no warning whatsoever.
With defineSlots, a large share of error detection shifts from testing in the browser to compile time, which noticeably saves time for frequently reused, generic components like lists, tables or layout wrappers. The effort for the additional type declaration is small, usually just a few lines, while the benefit grows with every additional place the component is later used across the project.
9. Common mistakes and conclusion
The most common mistake is calling defineSlots with a runtime argument instead of a pure type annotation, which simply does not work, since the macro accepts only generic type information and generates no runtime code of its own. A second common mistake is letting the slot binding in the component's
The takeaway: defineSlots
| Slot kind | defineSlots syntax | Optional? | Typical use |
|---|---|---|---|
| Default slot with scoped props | default(props: T): void | No, usually required | Rendering list and table rows individually |
| Named slot with scoped props | footer(props: T): void | Yes, via footer?(...) | Optional footer area with extra data |
| Pure content slot without props | header(): void | Yes, via header?(): void | Static markup with no data passed |
| Multiple slots at once | { default(...); footer?(...) } | Mixed possible | Layout components with several regions |
Mironsoft
Vue architecture, Composition API, and Nuxt performance
Vue applications that don't get more complicated with every feature?
We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.
Architecture Review
Checking composables, state management, and component structure for maintainability.
Performance Audit
Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.
Nuxt Integration
Building robust, type-safe SSR/SSG setup and API integration.
10. Summary
Typing Slots with TypeScript: The Essentials at a Glance
Syntax
defineSlots
Effect
A pure compile-time construct, taking effect via Volar both inside the component and in the consuming template.
Optional slots
A question mark after the slot name marks it optional, independent of the v-if="$slots.x" runtime check.
Benefit
Autocompletion for slot props right in the parent's template, typos become compile errors.