skip to content

In a generic Vue `<script setup generic="T">` List component, how do you declare a typed `item` slot so the parent's slot props are checked as `T`?

level: seniorimportance: should knowfreq 35%

answer

  1. a macro with only a type argument
  2. slot name maps to a function type
  3. first parameter is the slot props
  4. return type is currently ignored
  5. compiles down to useSlots()

basics

~20 s

Call defineSlots with a type literal such as item(props: { item: T; index: number }): any. Vue's language tools then check the child's slot element against it and type the parent's #item props as the inferred T; runtime behaviour is unchanged.

solid answer

~40 s

Inside the generic block, call `defineSlots<{ item(props: { item: T; index: number }): any }>()`. Each key is a slot name and each value a function type whose first parameter is that slot's props; the return type is currently ignored, so `any` is fine. The macro takes no runtime arguments (the compiler errors if you pass one) and compiles to `useSlots()`, so it only affects types: `vue-tsc` checks `<slot name="item" :item="row" :index="i">` against the declaration, and the parent's `#item="{ item }"` gets whatever `T` was inferred from `items`. Without `defineSlots` the language tools infer slot types from the template's `<slot>` elements, so declaring them is about owning an explicit contract that catches a wrong slot prop in the child and types `slots` in script. Outside SFCs, the equivalent is `slots: Object as SlotsType<...>` (3.3+).

code

ts · 16 lines
ts
import { defineComponent, h, type PropType, type SlotsType } from 'vue'

interface User { id: number; name: string }

export default defineComponent({
  props: { items: { type: Array as PropType<User[]>, required: true } },
  slots: Object as SlotsType<{
    item: { item: User; index: number }
  }>,
  setup(props, { slots }) {
    return () =>
      h('ul', props.items.map((item, index) =>
        h('li', { key: item.id }, slots.item?.({ item, index }))
      ))
  }
})

go deeper

for a junior

Recall that defineSlots declares slot names and the props each slot passes, using only a type argument.

for a middle

Explain the type literal: key is the slot name, value a function type whose first parameter is the slot props, return type ignored.

for a senior

Show that the macro is types only, compiles to useSlots(), and that vue-tsc is what enforces both the child's slot bindings and the parent's usage.

for a principal

Decide when a shared component library should state slot contracts explicitly rather than rely on template inference that silently follows refactors.

## The problem: slot props in a generic component A **scoped slot** lets a child hand data back to the content the parent renders inside it. In a generic `List<T>`, the natural API is an `item` slot that receives each element of `items`, so the parent can render a user as a card and a product as a row. The question is what type the parent's slot props get, and who guarantees that the child really passes them. In Vue 3.3+ the answer is the **`defineSlots`** compiler macro. ## Declaring the slot contract ```vue <script setup lang="ts" generic="T extends { id: string | number }"> defineProps<{ items: T[] }>() const slots = defineSlots<{ item(props: { item: T; index: number }): any default(props: {}): any }>() </script> <template> <ul> <li v-for="(row, i) in items" :key="row.id"> <slot name="item" :item="row" :index="i" /> </li> </ul> <slot /> </template> ``` The rules of the type literal: - Each **property key is a slot name** (`default` for the unnamed slot). - Each **value is a function type**; its first parameter is the props that slot passes. - The **return type is currently ignored** and can be `any`; the docs reserve it for possible slot-content checking later. - `T` from the `generic` attribute is in scope, so slot props share the type the parent's `items` fixed. ## What the parent sees ```vue <List :items="users"> <template #item="{ item, index }"> {{ index }}: {{ item.name }} </template> </List> ``` Because `users` is `User[]`, the Vue language tools infer `T = User`, and `item` in the slot is typed `User`. Reading `item.email` when `User` has no such field is a template type error in the **parent**. ## What the macro does and does not do | Aspect | Behaviour | |---|---| | Arguments | Type argument only; passing a runtime argument is a compile error (`defineSlots() cannot accept arguments`) | | Calls per component | One; a second call is a compile error | | Return value | The `slots` object, the same one `useSlots()` returns; the compiler rewrites the call to `useSlots()` | | Runtime validation | None: slot props are not checked in the browser | | Child-side check | `vue-tsc` checks each `<slot>` element's bound props against the declaration | | Parent-side typing | Slot props in `#item="{ ... }"` are typed from the declaration | ## Inference versus declaration A senior-level nuance: **without `defineSlots`, the Vue language tools already infer the slots type from the `<slot>` elements in the template**, so a parent often gets `T` for slot props anyway. Declaring the slots explicitly still earns its place: 1. **The contract is stated, not derived.** A refactor that renames `:item` to `:row` in the template becomes an error in the child instead of silently changing the public API. 2. **Script code gets typed slots.** `slots.item?.({ item, index })` in a helper or render logic is checked, which template inference does not give you. 3. **Documentation for consumers.** The slot API is readable at the top of the file, next to props and emits. ## Where the checks actually happen Because the macro produces no runtime code beyond `useSlots()`, every guarantee it gives is enforced by the type checker, in two directions: - **In the child**, each `<slot name="item" ...>` element is compared with the declared props. Binding `:row="row"` instead of `:item="row"`, or forgetting `:index`, is reported on that element. - **In the parent**, the destructured slot scope `#item="{ item, index }"` takes its types from the declaration, specialised to the `T` inferred from `items`. Reading a field `T` does not have is an error in the parent's template. - **Neither check runs in the browser.** A project that type-checks only in the editor, and never runs `vue-tsc` in CI, can merge a broken slot contract without any warning. That last point is the practical reason senior interviewers ask about `defineSlots`: the typed slot is only as strong as the type-checking step in the pipeline. ## Outside `<script setup>` For components written with `defineComponent` and render functions, Vue 3.3+ offers the `slots` option, whose runtime value is unused and whose type comes from a cast: `slots: Object as SlotsType<{ item: { item: User; index: number } }>`. Note the shape differs: `SlotsType` maps slot names to **props objects**, while `defineSlots` maps them to **function types**. ## Pitfalls - Expecting `defineSlots` to **warn at runtime** when a slot receives the wrong props: it is types only. - Passing a runtime object such as `defineSlots({ item: Function })`: a compile error. - Declaring the slot contract but binding different names on the `<slot>` element: `vue-tsc` reports it, the browser does not. - Treating the return type as a content check: it is currently ignored.

  • If a Vue SFC has no defineSlots call, do the parent's slot props become any?
    Usually not. The Vue language tools infer a slots type from the `<slot>` elements in the child's template, so a `<slot name="item" :item="row">` inside a `v-for` over `T[]` already yields `T`. `defineSlots` makes that contract explicit, checks the `<slot>` bindings against it and types the `slots` object in script.
  • What does Vue do at runtime when a parent's slot content reads a property the child never passed?
    Nothing special: `defineSlots` generates no validation, so the property is simply `undefined` in the slot scope. Only the type checker, `vue-tsc` or the editor, reports the mismatch, which is why generic slot contracts need type checking in CI.

saying these in an interview costs you the question

  • defineSlots validates slot props at runtime and warns on a mismatch.
  • defineSlots takes a runtime object of slot definitions like defineProps does.
  • The slot function's return type restricts what content the parent may pass.
  • Without defineSlots, slot props are always any in the parent.
  • SlotsType and defineSlots use the same shape: function types per slot.