In Vue Nation 2024…
There were several impressive sessions on Day 2
.
A slight disappointment was that there were many introductions to UI libraries
rather than technical solutions, which made it a bit boring.
However, seeing that even the libraries I didn’t use, have a lot of development plans for 2024
, I thought I should create an opportunity to use and introduce them later. (Prime Vue
and Vuetify
caught my interest!)
After Day 2, I gained the following insights, which I plan to introduce sequentially after this post!
- Build type-safe components (With generic types)
- Testing Vue 3 Apps with Playwright & Vitest
- Common mistakes in using Vue.js, and how to avoid them
First, let’s take a look at today’s topic, Build type-safe components (With generic types)
.
Generically Typed Components
In Vue 3.3 supports First of all, before this post, I would like to express my gratitude to Abdelrahman Awad
who made it possible for me to write this post.
It was such a great session!
Vue 3.3 was officially released in March 2023. One of the announcements was the support for Generically Typed Vue Components.
As explained in the above blog, you can now set type parameters through the generic
attribute in the <script setup>
tag. Like this.
<script setup lang='ts' generic='T'>
defineProps<{
items: T[].
selectedItem: T
}>()
</script>
Also, It can extend multiple parameters
, constraints, default types, and imported types using extends
as shown below.
<script setup lang='ts' generic='T extends string | number, U extends Item'>
import type { Item } from './types'
defineProps<{
label: T,
itemList: U[]
}>()
</script>
Through this, it clearly type-checks
whether two or more props passed to the component are actually same type. (If the types are different, Volar plugin
marks it with a red line!)
Let’s try it!
Below is a simple example I wrote myself.
(I used Nuxt UI in the example. I will post
about this later!)
Case 1: Input
// /components/A/Card.vue
<script setup lang="ts" generic="T extends string | number">
defineProps<{
label: T
cardType: T
}>()
const model = defineModel<T extends 'number' ? number : string>()
</script>
<template>
<p>
{{ label }}:
</p>
<div>
<DDInput
v-model="model"
:type="(cardType as string)"
/>
</div>
</template>
///page/index.vue
<script setup lang="ts">
const text = ref('')
const count = ref(0)
</script>
<template>
<div class="w-full min-h-screen flex justify-center items-center gap-8">
<div class="flex flex-col gap-2">
<ACard
v-model="text"
label="string"
:card-type="'string'"
/>
<pre>
{{ { value: text, type: typeof text } }}
</pre>
</div>
<div class="flex flex-col gap-2">
<ACard
v-model="count"
label="number"
:card-type="'number'"
/>
<pre>
{{ { value: count, type: typeof count } }}
</pre>
</div>
</div>
</template>
Above example is structured as follows.
ACard.vue
component receiveslabel
andcardType
as props and accepts a reactive value throughdefineModel
.- In
index.vue
, if a specific type of value is not passed, it is marked with a red line, and type inference is possible. Also, themodelValue
of theACard
component infers the necessary type based on the type provided byindex.vue
. (I searched, and it seems that the Volar VSCode plugin automatically indicates missing parameters, but it seems that I can’t do that, and I need to look into it more.)
(whats happened to my vscode?…)
Case 2: SelectMenu
Following example is simple one using SelectMenu
. This also allows type inference of the component using Generic Component.
// /components/A/List.vue
<script setup lang="ts" generic="T extends Option">
import type { Option } from '~/types/index'
const props = defineProps<{
modelValue: T
options: T[]
}>()
const emit = defineEmits<{
'update:model': [value: T]
}>()
const computedModel = computed({
get () {
return props.modelValue
},
set (value: T) {
emit('update:model', value)
}
})
</script>
<template>
<div>
<DDSelectMenu
v-model="computedModel"
:options="options"
>
<template #leading>
<span>
{{ computedModel.id }}
</span>
</template>
</DDSelectMenu>
</div>
</template>
// /pages/select.vue
<script setup lang="ts">
import type { Option } from '~/types/index'
const options: Option[] = [
{ id: 1, label: 'banana' },
{ id: 2, label: 'orange' },
{ id: 3, label: 'apple' },
{ id: 10, label: 'strowberry' }
]
const selectedOption = ref<Option>(options[0])
</script>
<template>
<div class="h-screen w-full flex flex-col justify-center items-center gap-8">
<AList
v-model="selectedOption"
:options="options"
@update:model="(value) => selectedOption = value"
/>
<pre>
{{ { value: selectedOption, type: typeof selectedOption } }}
</pre>
</div>
</template>
// /types/index.ts
export interface Option {
id: string | number
label: string
}
If you pass a value of different type to modelValue
, it will be marked with a red line like this, allowing for type inference.
// /pages/select.vue
<script setup lang="ts">
import type { Option } from '~/types/index'
const options: Option[] = [
{ id: 1, label: 'banana' },
{ id: 2, label: 'orange' },
{ id: 3, label: 'apple' },
{ id: 10, label: 'strowberry' }
]
const selectedOption = ref<Option>(options[0])
const selectedSometing = ref<{id: number, value: string}>({ id: 1, value: 'banana' })
</script>
<template>
<div class="h-screen w-full flex flex-col justify-center items-center gap-8">
<AList
v-model="selectedSometing"
:options="options"
@update:model="(value) => selectedOption = value"
/>
<pre>
{{ { value: selectedOption, type: typeof selectedOption } }}
</pre>
</div>
</template>
That’s all!
I haven’t fully using it yet, but I believe Generic Component
will greatly enhance DX experience
.
It seems worth checking why the feature in the Volar plugin
that infers missing parameters is not working properly!!
These were the insights I gained from this session.
If there are any issues or areas for improvement, please leave a comment
!
Feedback is always welcome!
See you next!