In March 2022, I built my first serious project with Vue 3 + Typescript. And I was flabbergasted - these two play together delightfully. It felt like there are worlds regarding developer experience between Vue 2 and Vue 3.
Among the benefits I noticed were:
- Excellent VSCode code completion
- Clever import suggestions for components and composable functions
- Convenient code abstractions
This article series is for you if you're:
- looking to make your Vue apps more robust
- are curious about how Vue 3 differs from previous versions
- use Typescript in your Vue apps
Already hooked? Great. Let's jump in!
Setting up the app with Vite
Vite allows for quick boilerplating. Open a terminal in a project folder of your choice. Then, type:
yarn create vite
# for NPM
npm create vite@latest
- Select vue as your framework
2. Choose vue-ts as your project variant
3. Run yarn
& yarn dev
to start the development server
The project's file structure
We receive the following boilerplate. There are at least two files that differ from a Javascript boilerplate. Let's have a look at them.
The env.d.ts
file
Despite its tiny size, this file is a powerhouse. It globally maps the DefineComponent
type to all .vue
files. That means Typescript Intellisense for all of your Vue components!
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
The tsconfig.json
file
Holds configuration of how the TS compiler interacts with your project. You don't need to make any changes here. Check out the official docs to learn more.
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
The HelloWorld.vue file
Let's have a look at the boilerplate component. If you haven't worked with Vue 3 yet - this template uses Vue's Composition API. I'll cover the details in another article. First, let's try to understand what's going on by comparing three different component setups.
- The common
options
API approach with a single, default component export - The common
composition
API approach with a setup method - The newer
composition
API approach with a setup 'attribute'
HelloWorld - Typescript + Options API
In a traditional Vue app, you declare a single default export. This is your component. It provides data and logic to your HTML template.
<script lang="ts">
export default {
data: () => ({
count: 0,
}),
props: {
msg: {
type: String,
required: true,
},
},
methods: {
increment() {
this.count++;
},
}
};
</script>
<template>
<h1>{{ msg }}</h1>
<button type="button" @click="increment">count is: {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
While this is a perfectly valid approach, try and imagine you have a single Vue file that exceeds one thousand lines of code. It will have all its states aggregated within a single object. Methods or computed properties are likely located further below. Jumping back and forth between state and logic quickly gets tedious.
HelloWorld - Typescript + Setup method
Behold the setup method. It essentially allows you to decouple data and logic and is the gateway to Vue's composition API.
It comes with a significant difference from the options API: The setup method is evaluated before the component is created. As a consequence, inside of setup
, you have no access to the Vue instance using this
.
Instead of using this.(...), you can access props and the component's context by two arguments of the setup function
The following code is the equivalent to the Options - API approach above.
- It uses
ref
to make the variablecount
reactive - It uses setup to provide
count
andincrement
to the template
<script lang="ts">
import { ref } from 'vue';
export default {
props: {
msg: {
type: String,
required: true,
},
},
setup(props, { attrs, emit, slots }) {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
},
};
</script>
<template>
<h1>{{ msg }}</h1>
<button type="button" @click="increment">count is: {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
Instead of having dedicated data
and method
sections, I declared both inside the setup method.
HelloWorld - Typescript + Setup syntactic sugar
Till now, we didn't really make a lot of Typescript usage. Even the props are declared using Object syntax. It's time to change this.
Before Vue 3's beta phase ended, this RFC provided syntactic sugar for the composition API. It also paved the way for the modern Vue+TS syntax I found so powerful.
With <script setup>...</script>
> There is no need to return any components, variables or functions to the template. They're simply available to the template.
Let's have a look at what this means in the code.
<script setup lang="ts">
import { ref } from 'vue';
defineProps<{ msg: string }>();
const count = ref(0);
const increment = () => count.value++;
</script>
<template>
<h1>{{ msg }}</h1>
<button type="button" @click="increment">count is: {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
That's just four lines of Typescript! As a bonus, types are automatically propagated into the parent component.
Try and assign the msg
property in the parent component.
Where's the rest?
What happened to props, emits, attributes and slots?
- Props and emits were turned into
compiler macros
, one of which isdefineProps
- Default props can be declared using
withDefaults
- Attributes and slots must be imported separately.
They're still available in the template using$attrs
&$slots
I'll describe these in a separate article in detail. If you're after a quick overview, have a look here:
<script setup lang="ts">
import { ref, withDefaults, useSlots, useAttrs } from 'vue';
const props = withDefaults(
defineProps<{
msg: string;
}>(),
{ msg: 'Hello World!' }
);
const emit = defineEmits<{
(event: 'click', count: number): void;
}>();
const slots = useSlots();
const attributes = useAttrs()
const count = ref(0);
const increment = () => count.value++;
</script>
<template>
<h1>{{ msg }}</h1>
<button type="button" @click="increment">count is: {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
Wrapup
So far we have:
- Created a Vue 3 - Typescript project
- Got a brief overview of TS - project-specific files
- Covered the difference between Vue's options and composition API
- Outlined how Typescript Intellisense helps us during development
In the follow-up articles, we'll dive even deeper into these topics and explore what else Vue's latest version has up its sleeves for us.