Skip to content
← All questions
Advanced

How do generic components work with TypeScript?

TypeScriptComponents

Generic components let you write a single component that works with different data types while keeping full type safety. The generic attribute on <script setup> (Vue 3.3+) declares type parameters that flow through props, emits, and slots.

Basic example

vue
<!-- GenericList.vue -->
<script setup lang="ts" generic="T">
defineProps<{
  items: T[]
  selected?: T
}>()

defineEmits<{
  select: [item: T]
}>()
</script>

<template>
  <ul>
    <li
      v-for="(item, index) in items"
      :key="index"
      @click="$emit('select', item)"
    >
      <slot :item="item" />
    </li>
  </ul>
</template>
vue
<!-- Parent.vue -->
<script setup lang="ts">
import { ref } from 'vue'

interface User {
  id: number
  name: string
}

const users = ref<User[]>([
  { id: 1, name: 'Ana' },
  { id: 2, name: 'Luis' }
])

function onSelect(user: User) {
  // user is typed as User, not unknown
  console.log(user.name)
}
</script>

<template>
  <GenericList :items="users" @select="onSelect">
    <template #default="{ item }">
      <!-- item is typed as User -->
      {{ item.name }}
    </template>
  </GenericList>
</template>

TypeScript infers T = User from the items prop. The select event and the slot's item are both typed as User automatically.

Multiple type parameters

vue
<script setup lang="ts" generic="T, U extends string">
defineProps<{
  data: T[]
  labelKey: U
}>()
</script>

Constraints

You can constrain generics with extends:

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

<template>
  <div v-for="item in items" :key="item.id">
    <!-- TypeScript knows item.id exists -->
    <slot :item="item" />
  </div>
</template>

Importing types in generic declarations

vue
<script setup lang="ts" generic="T extends BaseItem">
import type { BaseItem } from '@/types'

defineProps<{
  items: T[]
}>()
</script>

Before Vue 3.3

Without the generic attribute, you had to use defineComponent with a render function or JSX to get generic types, which was significantly more verbose:

tsx
import { defineComponent } from 'vue'

function createGenericList<T>() {
  return defineComponent({
    props: { items: Array as () => T[] },
    setup(props) {
      return () => (
        <ul>
          {props.items?.map(item => <li>{String(item)}</li>)}
        </ul>
      )
    }
  })
}

The generic attribute makes this pattern accessible with normal SFC templates.

When to use generic components

ScenarioGeneric?
List/table/select that works with any data typeYes
Form field wrapper with typed v-modelYes
Component that always works with a known typeNo, use concrete types
Simple UI components (buttons, cards, modals)No

Released under the MIT License.