Webpacker, Vue 3, and TypeScript
Tags: vue, webpack, rails, typescript
Published: 2021-03-17 08:07:16

___________
Versions:

Webpacker 5
Rails 6
Vue 3
___________

## Foreword

Compared to Vue 2, Vue 3 is written in TypeScript. As we're used to, the official Vue Documentation is one of the best sources to find out more about [how to configure TypeScript in Vue 3](https://v3.vuejs.org/guide/typescript-support.html#official-declaration-in-npm-packages). Something that can bother is that most tutorials use the [Vue CLI](https://cli.vuejs.org/) to show how simple TypeScript can be added to the codebase. Although the Vue CLI is a powerful tool and it is actually as simple as running one command to add TypeScript, not each project has the possibility to be configured with the CLI or [Vite](https://vitejs.dev/). This article explains how to add TypeScript to Vue 3 applications within [Webpacker](https://github.com/rails/webpacker), the [Webpack](https://webpack.js.org/) gem for [Ruby on Rails](https://rubyonrails.org/) Fullstack applications.

## How to configure TypeScript in Vue 3

### 1. TS Loader

To install the TypeScript Loader, run:

```bash
yarn add ts-loader
# or npm ts-loader
```

### 2. TS Config

In the root directory of the Rails App, create the file `tsconfig.json`. The following JSON is an example of the configuration you could add. Of course, your settings might differ from those. Make sure to adapt the paths which files to **include** in the TypeScript Compilation (`app/javascript/src/**/*.ts` and `app/javascript/src/**/*.vue`) depending on your folder structure. Same for the **alias** in the paths settings (`app/javascript/src/*`)

```json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "baseUrl": ".",
    "types": [
      "webpack-env"
    ],
    "paths": {
      "@/*": [
        "app/javascript/src/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
  "include": [
    "app/javascript/src/**/*.ts",
    "app/javascript/src/**/*.vue",
  ],
  "exclude": [
    "node_modules"
  ]
}
```

**HAVE YOU CHECKED THE PATHS? NO? READ ABOVE THE CONFIG AFTER COPY/PASTING! ;)**

### 3. Webpack Loader

As explained in a previous article about [How to add Vue 3 in Rails](https://dev.to/vannsl/vue3-on-rails-l9d) I put all webpack loaders in a folder called `config/webpack/loaders`. You can also create your loaders inline.

The Loader Configuration is:

```js
module.exports = {
  test: /\.tsx$/,
  loader: "ts-loader",
  options: {
    appendTsSuffixTo: [/\.vue$/],
  },
  exclude: /node_modules/,
};
```

In the webpack configuration `config/environment.js` add the loader:

```js
const ts = require("./loaders/ts");

// ...

environment.loaders.prepend("ts", ts);
```

Only for reference, this is how my full webpack configuration looks like:

```js
const { environment } = require("@rails/webpacker");
const { VueLoaderPlugin } = require("vue-loader");
const { DefinePlugin } = require("webpack");
const path = require("path");
const vue = require("./loaders/vue");
const ts = require("./loaders/ts");

const customConfig = {
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "..", "..", "app/javascript/src"),
      "~libs": path.resolve(__dirname, "..", "..", "app/javascript/lib"),
      "~images": path.resolve(__dirname, "..", "..", "app/javascript/images"),
    },
  },
};

environment.config.merge(customConfig);

environment.plugins.prepend("VueLoaderPlugin", new VueLoaderPlugin());
environment.plugins.prepend(
  "Define",
  new DefinePlugin({
    __VUE_OPTIONS_API__: false,
    __VUE_PROD_DEVTOOLS__: false,
  })
);
environment.loaders.prepend("ts", ts);
environment.loaders.prepend("vue", vue);
environment.splitChunks();
module.exports = environment;
```

### 4. Shims

To get the TypeScript Support working in Vue Single File Components, they have to be defined as a component. Quoting the official documentation about [defineCompinent](https://v3.vuejs.org/api/global-api.html):

> Implementation-wise defineComponent does nothing but return the object passed to it. However, in terms of typing, the returned value has a synthetic type of a constructor for manual render function, TSX, and IDE tooling support.

In your folder where your Vue Applications are located (e.g. `app/javascript/src`), add the file `shims-vue.d.ts` to add the Shim:

```ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}
```

### 5. Linters and IDE Helpers

This is up to you. I use ESLint and Prettier. For IDE Support I switched from [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) to [Vue DX](https://marketplace.visualstudio.com/items?itemName=znck.vue-language-features), but I can't strongly agree that you should do the same. The third member of the IDE Party is [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar), which I can totally recommend for pure Vue3+TS Projects, especially if you use the `<script setup>` syntactic sugar for using the [Composition API](https://v3.vuejs.org/guide/composition-api-introduction.html). Try them out and check what works best for you.

🎉 **You're done!**

## Usage

### Vue and TS

Using Typescript in `.vue` files require the following steps:

1. Add `lang="ts"` to the `<script>` tag
2. Import `defineComponent` from `vue`
3. Export the component as `defineComponent`

Example:

```html
<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  // ...
})
```

### Type Declarations

Types can be found in file `types.ts` the source directory:

```ts
// app/javascript/src/types.ts

export interface Item {
  id: number;
  name?: string;
  // ...
}
```

Import types from that file:

```ts
import { Item } from "@/types";
```

### Vue Data and Properties

#### Type Assertions

Using the `as` keyword objects were able to override empty objects types to a real type:

```ts
const futureItem = {} as Item

futureItem.id = 1;
futureItem.name = "New Item"
```

#### Vue Reactive Data Options API

With that, we can assign types to data attributes in `.vue` files:

```html
<script lang="ts">
import { Item } from "@/types";
import { defineComponent } from "vue";

export default defineComponent({
  data() {
    return {
      items: [] as Item[],
      currentItem: {} as Item
    }
  }
})
</script>
```

#### Vue Reactive Data Composition API

TODO :)

#### Vue Properties

The same does not simply work for Vue Properties. Using the `PropType`, Generics are set for custom properties.

```ts
// Before
props: {
  item: {
    type: Object,
    required: true
  }
}

// Won't work
props: {
  item: {
    type: Item, // this is not valid JavaScript
    required: true
  }
}

// Won't work
props: {
  item: {
    type: Object as Item, // valid JavaScript, but no generic
    required: true
  }
}

// Works
import { defineComponent, PropType} from "vue";
import { Item } from "@/types";

export default defineComponent({
  props: {
    item: {
      type: Object as PropType<Item>,
      required: true
    }
  }
}
```

#### Vue Computed Properties and Methods Options API

Computed Properties and Methods don't need special TypeScript Handling in Vue.
Types can be applied as usual in TypeScript:

```ts
import { defineComponent, PropType} from "vue";
import { Item } from "@/types";

export default defineComponent({
  data() {
    return {
      items: [] as Item[],
      currentItem: {} as Item
    }
  },
  // Method Parameter types
  methods: {
    function addItem(newItem: Item) {
      this.items.push(newItem)
    }
  },
  // Computed Property Return Item
  computed: {
    firstItem(): Item {
      return this.items[0]
    }
  }
})
```

## Eh, and now?

To learn how to use TypeScript ➡️ (e)Book [TypeScript in 50 lessons](https://www.smashingmagazine.com/printed-books/typescript-in-50-lessons/#:~:text=In%20TypeScript%20in%2050%20Lessons,explained%2C%20from%20start%20to%20finish.)