Writing Generic Componets In Vue 3
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