Back

Writing Generic Componets In Vue 3

Vue 3 Generics

Introduction

Up until now, Vue has lacked a native mechanism for handling generics. Why is this important? Because generics enable developers to build more flexible and type-safe components that can handle diverse data types. This is especially useful for building reusable components that need to adapt to different data types seamlessly. Thankfully, Vue 3.3 introduces generics! Which allow developers to define generic props and emits for components.

Before the introduction of generics in Vue 3.3, developers faced a challenge when it came to building reusable components that could handle diverse data types with type safety. While Vue excelled at component composition and reusability, ensuring type safety and handling different data types within components was, let's say, not great. Developers often resorted to using any types or complex workarounds.

To best understand how generics work in Vue 3.3, we'll walk through a simple example of a vue list component with props, slots and emits.

List Component Example

let's assume we want to build a list component, how would you normally build it? Probably something like this:

<template>
  <div>
    <h2>Total Items: {{ items.length }}.</h2>
    <ul>
      <li v-for="item in items" :key="item.id">
        <slot :item="item" :isSelected="isSelected(item)">
          {{ item }}
        </slot>
      </li>
    </ul>
    <h2>
      Selected Item:
      <slot name="selected-item" :item="selected">
        {{ selected }}
      </slot>
    </h2>
  </div>
</template>

<script setup lang="ts"">
const props = defineProps<{
  items: any[] // or Record<string, any>[]
  selected: any; // or Record<string, any>
}>();

defineEmits<{
  (e: "update:selected", value: any): void;
}>();

const isSelected = (item: any) => item === props.selected;
</script>

this is a pretty standard list component, it has an items prop, a selected prop and an update:selected emit. The items prop is an array of type any.

The way consume this list component is typically like this:

<template>
  <MyList v-model:selected="selected" :items="users">
    <template #selected-item="{ item }">
      <strong>{{ item.name }}</strong>
    </template>
    <template #item="{ item, isSelected }">
      <!-- item has type "any" even though we know it's an object with an id and name property -->
      <span :class="{ selected: isSelected }">{{ item.name }}</span>
    </template>
  </MyList>
</template>

<script setup lang="ts">
const users = ref([
  { id: 1, name: "John" },
  { id: 2, name: "Jane" },
  { id: 3, name: "Jack" },
]);
</script>

The problem with this approach is that the item prop is of type any even though we know it's an object with an id and name property. Usually either you would have to cast the item prop to the correct type or rely on any type and lose type safety completely.

Generics to the rescue

With generics, we can define the items prop as an array of type T and the selected prop as type T. This way, we can ensure that the item prop is of type T and not any. Let's see how this would look like:

<!-- <template> section remains unchanged -->

<script setup lang="ts" generic="T extends { id: string }">
const props = defineProps<{
  items: T[];
  selected: T;
}>();

defineEmits<{
  (e: "update:selected", value: T): void;
}>();

const isSelected = (item: T) => item === props.selected;
</script>

The items prop is now an array of type T and the selected prop is of type T. The T type is constrained to an object with an id property. This way, we can ensure that the item prop is of type T and not any.

Vue automatically infers the T type from the items prop and the selected prop for both the emits and the slots as well. This means that when we consume the list component, the item prop will be of type T and not any:

Before / After

Now in my code base, if I try to access the item property, I get type safety and code completion and I'm protected from future refactoring mistakes, for example, if I change the name property to fullName in the users array, then i would get a type error!

Bottom Line

Thanks to generics, we can now build truly reusable components that handle diverse data types with type safety. This is especially useful for component libraries and design systems.

A list component is just one example, but there are many other use cases for generics. For example, you can use generics to build a generic form component that can handle different data types with type safety. Or you can use generics to build a generic table component that can handle different data types with type safety, without being tied to a specific data structure.

Got any questions or feedback? Feel free to reach out to me on Bento

Follow for #vue, #typescript and #dx topics