---
title: "Typed slots, defineExpose and component instances · vuejs.school"
description: "Type slot props with defineSlots, give a child a small typed API with defineExpose, and reach it from the parent with useTemplateRef, generics included."
canonical_url: "https://vuejs.school/community/blog/typed-slots-defineexpose-and-component-instances"
meta:
  "og:description": "Type slot props with defineSlots, give a child a small typed API with defineExpose, and reach it from the parent with useTemplateRef, generics included."
  "og:image": "https://vuejs.school/_og/d/a_~VHlwZWQgc2xvdHMsIGRlZmluZUV4cG9zZSBhbmQgY29tcG9uZW50IGluc3RhbmNlcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHlwZWQgc2xvdHMsIGRlZmluZUV4cG9zZSBhbmQgY29tcG9uZW50IGluc3RhbmNlcw%2Cdescription_~VHlwZSBzbG90IHByb3BzIHdpdGggZGVmaW5lU2xvdHMsIGdpdmUgYSBjaGlsZCBhIHNtYWxsIHR5cGVkIEFQSSB3aXRoIGRlZmluZUV4cG9zZSwgYW5kIHJlYWNoIGl0IGZyb20gdGhlIHBhcmVudCB3aXRoIHVzZVRlbXBsYXRlUmVmLCBnZW5lcmljcyBpbmNsdWRlZC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBlZC1zbG90cy1kZWZpbmVleHBvc2UtYW5kLWNvbXBvbmVudC1pbnN0YW5jZXMi%2Cs_zFB0CvjVPL2R-M9E.png"
  "og:image:alt": "Typed slots, defineExpose and component instances · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Typed slots, defineExpose and component instances · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~VHlwZWQgc2xvdHMsIGRlZmluZUV4cG9zZSBhbmQgY29tcG9uZW50IGluc3RhbmNlcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHlwZWQgc2xvdHMsIGRlZmluZUV4cG9zZSBhbmQgY29tcG9uZW50IGluc3RhbmNlcw%2Cdescription_~VHlwZSBzbG90IHByb3BzIHdpdGggZGVmaW5lU2xvdHMsIGdpdmUgYSBjaGlsZCBhIHNtYWxsIHR5cGVkIEFQSSB3aXRoIGRlZmluZUV4cG9zZSwgYW5kIHJlYWNoIGl0IGZyb20gdGhlIHBhcmVudCB3aXRoIHVzZVRlbXBsYXRlUmVmLCBnZW5lcmljcyBpbmNsdWRlZC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBlZC1zbG90cy1kZWZpbmVleHBvc2UtYW5kLWNvbXBvbmVudC1pbnN0YW5jZXMi%2Cs_zFB0CvjVPL2R-M9E.png"
  "twitter:image:alt": "Typed slots, defineExpose and component instances · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Typed slot props with defineSlots

A list component that lets the parent decide how each row looks is the classic scoped slot: the child loops, the parent renders. Make it generic so it works for any item with an `id`:

*src/components/DataList.vue*

```
<script setup lang="ts" generic="T extends { id: string | number }">
defineProps<{ items: T[] }>()

defineSlots<{
  default(props: { item: T; 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>
```

`generic="T extends ..."` declares a type parameter for the whole component. `defineSlots` takes only a type: each key is a slot name, and the first argument of each function is the props that slot receives. The return type is ignored for now, so `any` is what the [Vue docs](https://vuejs.org/api/sfc-script-setup.html#defineslots) use. The question mark makes `empty` optional, so a parent does not have to fill it.

In the parent, `T` is inferred from the `items` you pass, so the slot props arrive fully typed:

*src/pages/TaskPage.vue*

```
<script setup lang="ts">
import DataList from '../components/DataList.vue'

type Task = { id: number; title: string; done: boolean }

const tasks: Task[] = [
  { id: 1, title: 'Read the lesson', done: true },
  { id: 2, title: 'Take the quiz', done: false },
]
</script>

<template>
  <DataList :items="tasks">
    <template #default="{ item, index }">
      {{ index + 1 }}. {{ item.title }} <span v-if="item.done">(done)</span>
    </template>
    <template #empty>Nothing to do.</template>
  </DataList>
</template>
```

Mistype `item.titel` and `vue-tsc` stops the build with `Property 'titel' does not exist on type 'Task'. Did you mean 'title'?`.

The Vue language tools can infer slot props from the `<slot>` tags alone. Declaring them still pays, because `defineSlots` turns that inference into a contract, and the child is checked against it too. Drop `:index` from the `<slot>` tag and the error appears in `DataList.vue` itself: `Argument of type '{ item: T; }' is not assignable to parameter of type '{ item: T; index: number; }'`.

The macro also returns a typed `slots` object. Keep it (`const slots = defineSlots<...>()`) when script code needs to check whether a slot was filled, such as `slots.empty`.

## A small public API with defineExpose

A component written with `<script setup>` is closed by default. A parent that holds a ref to it sees none of its variables or functions. To open a door, you call `defineExpose` with exactly what the parent may use.

Focusing an input is the textbook case, because focus is something you do, not a value you pass:

*src/components/SearchField.vue*

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

const query = defineModel<string>({ default: '' })
const inputEl = useTemplateRef('input')

function focus() {
  inputEl.value?.focus()
}

function clear() {
  query.value = ''
  focus()
}

defineExpose({ focus, clear })
</script>

<template>
  <input ref="input" v-model="query" type="search" aria-label="Search tasks">
</template>
```

`useTemplateRef('input')` connects to the element with `ref="input"`, and its type, `HTMLInputElement | null`, is inferred from the template. The value is `null` until the component mounts, hence the `?.`. The exposed object has two functions. `query` stays private: the parent already controls it through `v-model`.

## Call the child through a typed template ref

The parent uses the same `useTemplateRef`, this time on the component tag:

*src/App.vue*

```
<script setup lang="ts">
import { onMounted, ref, useTemplateRef } from 'vue'
import SearchField from './components/SearchField.vue'

const query = ref('')
const search = useTemplateRef('search')

onMounted(() => {
  search.value?.focus()
})
</script>

<template>
  <SearchField ref="search" v-model="query" />
  <button type="button" @click="search?.clear()">Clear</button>
</template>
```

In the template, `search` is unwrapped like any top-level ref, so the button calls `search?.clear()` without `.value`.

`vue-tsc` and your editor infer the type of a static template ref (a plain `ref="search"`, not a bound `:ref`) from the tag it sits on. Here `search.value` is the child's public instance: the exposed `focus` and `clear`, plus the built-in `$` properties every component has, such as `$el`. Try `search.value?.query` and you get `Property 'query' does not exist on type ...`, which is the closed-by-default rule enforced at compile time.

When you want the type spelled out, for example in a composable that receives the ref, use TypeScript's `InstanceType` on the component's type: `useTemplateRef<InstanceType<typeof SearchField>>('search')`. `typeof SearchField` is the component's constructor type, and `InstanceType` turns it into the type of what the ref holds. If any component will do and you only need shared properties such as `$el`, use `ComponentPublicInstance` from `vue`.

## Type a generic component's ref with ComponentExposed

Give `DataList` an exposed method, say `defineExpose({ scrollToTop })`, and put `ref="list"` on its tag in `TaskPage.vue`. Inference still works: `useTemplateRef('list')` knows about `scrollToTop` with no extra types. Generic components only differ when you write the type yourself:

```
const list = useTemplateRef<InstanceType<typeof DataList>>('list')
```

`vue-tsc` rejects it with `error TS2344`, saying the type `does not satisfy the constraint 'abstract new (...args: any) => any'`. A generic component compiles to a generic function type, not a constructor, so `InstanceType` has nothing to work on. The [Vue docs](https://vuejs.org/guide/typescript/composition-api.html#typing-component-template-refs) point to `ComponentExposed` from the `vue-component-type-helpers` package, which ships from the same repository as `vue-tsc`:

*src/pages/TaskPage.vue*

```
import { useTemplateRef } from 'vue'
import type { ComponentExposed } from 'vue-component-type-helpers'
import DataList from '../components/DataList.vue'

const list = useTemplateRef<ComponentExposed<typeof DataList>>('list')

function backToTop() {
  list.value?.scrollToTop()
}
```

Install the package as a dev dependency with `npm install -D vue-component-type-helpers`. It contains types only. The same package has `ComponentProps`, `ComponentSlots` and `ComponentEmit` for the other channels.

## Typing a dynamic component

`<component :is>` renders whichever component you give it. Type the value as `Component` from `vue`:

*src/components/SettingsTabs.vue*

```
<script setup lang="ts">
import { ref, type Component } from 'vue'
import AlertsTab from './AlertsTab.vue'
import BillingTab from './BillingTab.vue'
import ProfileTab from './ProfileTab.vue'

type TabKey = 'profile' | 'billing' | 'alerts'

const tabs: Record<TabKey, { label: string; component: Component }> = {
  profile: { label: 'Profile', component: ProfileTab },
  billing: { label: 'Billing', component: BillingTab },
  alerts: { label: 'Alerts', component: AlertsTab },
}

const active = ref<TabKey>('profile')
</script>

<template>
  <button v-for="(tab, key) in tabs" :key="key" type="button" :aria-pressed="active === key" @click="active = key">
    {{ tab.label }}
  </button>
  <component :is="tabs[active].component" />
</template>
```

The reactive state is the key, a string union, while the components sit in a plain constant. If you do store a component in state, use `shallowRef<Component>(ProfileTab)`. A normal `ref` makes the component object deeply reactive, and Vue warns: `Vue received a Component that was made a reactive object`.

A template ref on a dynamic component cannot be inferred, because the tools do not know which component will render. Put `ref="tab"` on the `<component>` tag and the value of `useTemplateRef('tab')` is typed as a bare `{}`, with nothing on it to call, so write the union yourself: `useTemplateRef<InstanceType<typeof ProfileTab> | InstanceType<typeof BillingTab>>('tab')`.

## Prefer props and events to exposed state

A typed ref makes calling into a child feel safe, and that is the trap. Here a parent wants the Delete button disabled while a confirmation dialog is open, and the dialog only exposes `open()`:

*src/pages/AccountPage.vue**Breaks***

```
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import ConfirmDialog from '../components/ConfirmDialog.vue'

const dialog = useTemplateRef('dialog')

function deleteAccount() {
  console.log('Account deleted')
}
</script>

<template>
  <button type="button" :disabled="dialog?.isOpen" @click="dialog?.open()">Delete account</button>
  <ConfirmDialog ref="dialog" @confirm="deleteAccount" />
</template>
```

`vue-tsc` reports `Property 'isOpen' does not exist ... Did you mean 'open'?`, and at runtime the button is never disabled. Exposing `isOpen` as well would silence the error, but then the open state lives in the child, and the parent reads it through a ref that is `null` on the first render.

Move the state up instead. Rewrite the dialog so it receives its open state through `v-model:open`, closes itself by setting the model to `false`, and emits `confirm`:

*src/components/ConfirmDialog.vue*

```
<script setup lang="ts">
const open = defineModel<boolean>('open', { required: true })
const emit = defineEmits<{ confirm: [] }>()

function confirm() {
  emit('confirm')
  open.value = false
}
</script>

<template>
  <dialog :open="open">
    <p>Delete your account?</p>
    <button type="button" @click="open = false">Cancel</button>
    <button type="button" @click="confirm">Delete</button>
  </dialog>
</template>
```

`required: true` types `open` as `boolean` rather than `boolean | undefined`, and a parent that leaves out `v-model:open` fails the type check. The parent now owns the boolean:

*src/pages/AccountPage.vue**Works***

```
<script setup lang="ts">
import { ref } from 'vue'
import ConfirmDialog from '../components/ConfirmDialog.vue'

const confirming = ref(false)

function deleteAccount() {
  console.log('Account deleted')
}
</script>

<template>
  <button type="button" :disabled="confirming" @click="confirming = true">Delete account</button>
  <ConfirmDialog v-model:open="confirming" @confirm="deleteAccount" />
</template>
```

No ref, no `null` check, and the dialog can be tested on its own by passing a prop. Keep `defineExpose` for actions that have no natural state: focus an input, scroll a list, play a video, reset a form to its defaults.

**Check it stuck**3 questions from the Generic components, slots and expose quiz.

[**Practice now**](https://vuejs.school/learn/typescript/generic-components-and-slots?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 6 min read**TypeScript strict mode in a Vue project: the errors you'll hit and the fixes**](https://vuejs.school/community/blog/typescript-strict-mode-in-a-vue-project) [Release notes · 7 min read**TypeScript 7.0: the Go compiler lands, and what it means for vue-tsc**](https://vuejs.school/community/blog/typescript-7-the-go-compiler-lands)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/typed-slots-defineexpose-and-component-instances#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-05-28","description":"Type slot props with defineSlots, give a child a small typed API with defineExpose, and reach it from the parent with useTemplateRef, generics included.","name":"Typed slots, defineExpose and component instances","url":"https://vuejs.school/community/blog/typed-slots-defineexpose-and-component-instances","breadcrumb":{"@id":"https://vuejs.school/community/blog/typed-slots-defineexpose-and-component-instances#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/typed-slots-defineexpose-and-component-instances"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/typed-slots-defineexpose-and-component-instances#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-05-28","description":"Type slot props with defineSlots, give a child a small typed API with defineExpose, and reach it from the parent with useTemplateRef, generics included.","headline":"Typed slots, defineExpose and component instances","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~VHlwZWQgc2xvdHMsIGRlZmluZUV4cG9zZSBhbmQgY29tcG9uZW50IGluc3RhbmNlcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHlwZWQgc2xvdHMsIGRlZmluZUV4cG9zZSBhbmQgY29tcG9uZW50IGluc3RhbmNlcw%2Cdescription_~VHlwZSBzbG90IHByb3BzIHdpdGggZGVmaW5lU2xvdHMsIGdpdmUgYSBjaGlsZCBhIHNtYWxsIHR5cGVkIEFQSSB3aXRoIGRlZmluZUV4cG9zZSwgYW5kIHJlYWNoIGl0IGZyb20gdGhlIHBhcmVudCB3aXRoIHVzZVRlbXBsYXRlUmVmLCBnZW5lcmljcyBpbmNsdWRlZC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBlZC1zbG90cy1kZWZpbmVleHBvc2UtYW5kLWNvbXBvbmVudC1pbnN0YW5jZXMi%2Cs_zFB0CvjVPL2R-M9E.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/typed-slots-defineexpose-and-component-instances#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/typed-slots-defineexpose-and-component-instances#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/typed-slots-defineexpose-and-component-instances#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Typed slots, defineExpose and component instances","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~VHlwZWQgc2xvdHMsIGRlZmluZUV4cG9zZSBhbmQgY29tcG9uZW50IGluc3RhbmNlcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHlwZWQgc2xvdHMsIGRlZmluZUV4cG9zZSBhbmQgY29tcG9uZW50IGluc3RhbmNlcw%2Cdescription_~VHlwZSBzbG90IHByb3BzIHdpdGggZGVmaW5lU2xvdHMsIGdpdmUgYSBjaGlsZCBhIHNtYWxsIHR5cGVkIEFQSSB3aXRoIGRlZmluZUV4cG9zZSwgYW5kIHJlYWNoIGl0IGZyb20gdGhlIHBhcmVudCB3aXRoIHVzZVRlbXBsYXRlUmVmLCBnZW5lcmljcyBpbmNsdWRlZC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBlZC1zbG90cy1kZWZpbmVleHBvc2UtYW5kLWNvbXBvbmVudC1pbnN0YW5jZXMi%2Cs_zFB0CvjVPL2R-M9E.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~VHlwZWQgc2xvdHMsIGRlZmluZUV4cG9zZSBhbmQgY29tcG9uZW50IGluc3RhbmNlcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~VHlwZWQgc2xvdHMsIGRlZmluZUV4cG9zZSBhbmQgY29tcG9uZW50IGluc3RhbmNlcw%2Cdescription_~VHlwZSBzbG90IHByb3BzIHdpdGggZGVmaW5lU2xvdHMsIGdpdmUgYSBjaGlsZCBhIHNtYWxsIHR5cGVkIEFQSSB3aXRoIGRlZmluZUV4cG9zZSwgYW5kIHJlYWNoIGl0IGZyb20gdGhlIHBhcmVudCB3aXRoIHVzZVRlbXBsYXRlUmVmLCBnZW5lcmljcyBpbmNsdWRlZC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy90eXBlZC1zbG90cy1kZWZpbmVleHBvc2UtYW5kLWNvbXBvbmVudC1pbnN0YW5jZXMi%2Cs_zFB0CvjVPL2R-M9E.png"}]}
```