An introduction to Vue 3 and Typescript - Getting started

Tobias Quante

Tobias Quante

Vue 3 has been completely rewritten in Typescript. Okay. Now, what does that mean for your next web project? This article series will shine a light on what makes Vue 3 so different.

Table of contents

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
  1. 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 variable count reactive
  • It uses setup to provide count and increment 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 is defineProps
  • 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.

Share this article