DEV Community

Cover image for Build Type-Safe Components with Vue 3.3
Vincent
Vincent

Posted on

Build Type-Safe Components with Vue 3.3

Five days ago, Vue version 3.3 "Rurouni Kenshi" was officially released and it's packed with amazing features enhancing DX and working with Typescript. In this post we are going to look at some of the cool new features and how the new Generic Types feature can help you to build better type-safe components.

What are Generic Types?

You can use Generic Types to check whether two or more props passed to a component do actually have the same type. This could be required when for example building a Select component, as shown in the following example from the official blog post from Evan You:

<script setup lang="ts" generic="T">
defineProps<{
  items: T[]
  selected: T
}>()
</script>
Enter fullscreen mode Exit fullscreen mode

This means, when supplying an input for the items property of type array<string> you would have to supply a value of type string or else Typescript would yell at you.


But when do I actually need this?!

The example above might feel a bit abstract, so let's look at a "real world" scenario where this could actually come in handy.

We are assuming an application, where we are fetching data with different models from the server as for example users and roles. We might wanna use a Select component that works with both types of data. Usually I would also add a Headless component library like Headless UI.

Generic Types in Action

To set up a quick demo we run:

npx create-vite@latest vue-demo --template vue-ts
cd vue-demo
npm install
Enter fullscreen mode Exit fullscreen mode

To set up Headless UI: npm install @headlessui/vue

The Select component

We can create a select component that use a Generic Type T that extends an object of type OptionProps = {id: string | number, name: string}, as follows:

<script setup lang="ts" generic="T extends OptionProps">
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel, } from "@headlessui/vue";
import { computed } from "vue";

// define and export Option props
export type OptionProps = {
  id: string | number;
  name: string;
};

// define props by using generic
const props = defineProps<{
  options: T[];
  modelValue: T;
}>();

// define emits
const emit = defineEmits<{
  "update:modelValue": [value: T];
}>();

// define proxyValue for modelValue to emit update:modelValue
const proxy = computed({
  get() {
    return props.modelValue;
  },
  set(value: T) {
    emit("update:modelValue", value);
  },
});
</script>
<template>
  <Listbox as="div" v-model="proxy">
    <ListboxLabel> Select an option </ListboxLabel>
    <ListboxButton> {{ proxy.name }}  </ListboxButton>
    <ListboxOptions>
      <ListboxOption v-for="option in options" :key="option.id" :value="option">
        {{ option.name }}
      </ListboxOption>
    </ListboxOptions>
  </Listbox>
</template>
Enter fullscreen mode Exit fullscreen mode

Now that we have defined our <MySelect /> component we can start using it in other places. This is where the power of typescript comes into play.

1. We have the awesome inline-type hinting by Volar extension

This is clearly one of my most favorite features of using Vue with Typscript. When some props on a component are required, Volar will inline hint them that they need to be included.

Inline type hinting


2. We will get notified when we our input does not satisfy the required prop type

By using the generic T, and defining the required props we know when we are missing some props on the supplied input.

Not required type


3. We can import OptionProps from the component

To make sure that we supply the correct type to the component, we can define an interface User that does extend our required type OptionProps from <MySelect />. This feature of importing types has also just been added recently.

Import type from OptionProps


4. We can make sure that our modelValue actually matches the input

As the modelValue requires an initial value of the type supplied with the options prop, it complains as the property supplied in this case is not valid.

Wrong type modelValue


That's a wrap!

I really think that Vue 3.3 is a huge boost for the overall DX when using Typescript and building type-safe components. For me personally this will be a huge boost in productivity and I am looking forward to integrate this in my future projects.

What do you think? Are you actually using Typescript with Vue. Have you been using any of the new features yet? And do you think you will find so place where you want to apply Generics to strongly type your components?

In case you liked this article, I would be happy if you'd left a like or to hear from you.

Thanks for reading and happy hacking! ❤️ 💻

Top comments (2)

Collapse
 
tomfun profile image
Greg • Edited

I don't really understand how to use it! I've update vue and tried to write a gereic component, but TS always thor errors!
Furthermore how to pin the type:

<script setup lang="ts" generic="T extends { timeSecond: number; }">

const props = defineProps({
  modelValue: {
    required: false,
    default() {
      return [] as T[];
    },
  },
// ...
Enter fullscreen mode Exit fullscreen mode

In my another component I want to use not a generic component, like Array<T>, but specific one Array<{ timeSecond: number; report: string; }> so I want to distinguish 2 components:

  • generic one
  • report component (report: string)
  • alert component (alert: string)
Collapse
 
vincentdorian profile image
Vincent

Hi Greg,

Sorry I do only answer to this now - I haven't been very active on here lately.
I hope you have found a solution now already.

I think for TS to recognize the type of the props you need to use the following notation const props = defineProps<{modelValue?: T}>().

As for the other problem you have mentioned I think you would do something with extending your generic type like: Array<T & {report: string}>