Pinnacles National Park, California (2018)

Pinnacles National Park, California (2018)

How to use Vue with TypeScript

The Secret Instructions that the Official Docs won't tell you.

(Updated )

First, read the relevant parts of the official docs. Following their instructions for configuring tsconfig.json, <script setup lang=ts>, etc. will get you like 99% of the way there.

This article is going to look at some of the gaps that are missing from the official documentation.

Disclaimer. I assume that you, like me, are not following their official recommendation to use create-vue. Maybe that would solve all of the problems outlined in this article. But I generally don’t like using pre-built boilerplate, if I can avoid it. I prefer building my own config files so I can understand how everything in my project works.

Table of Contents:

1. Deal with aliases

The official docs do address this topic, but I want to make it more explicit.

Let’s say you make an alias within your vite.config.js.

vite.config.js
...
resolve: {
alias: {
'@': resolve('./django_vue_experiments/vite_assets'),
'vue': 'vue/dist/vue.esm-bundler.js',
},
},
root: resolve('./django_vue_experiments/vite_assets'),
...

This is great. Because now instead of having to import my files with relative paths

import Thing from "../../thing.ts" 😕

or super long absolute paths

import Thing from "django_vue_experiments/vite_assets/thing.ts" 😰

we can instead use an alias

import Thing from "@/thing.ts"

But there is one more step. Vite knows about our alias, but the TypeScript compiler does not.

In your tsconfig.js you need to add your alias to compilerOptions.path:

tsconfig.js
{
"compilerOptions": {
...
"paths": {
"@/*": ["django_vue_experiments/vite_assets/*"]
}
}
}

Now TypeScript will know how to handle imports from your '@' alias.

2. Run type-checking with pre-commit

When building for production, Vite will apply type-checking and let you know if there are any errors with your TypeScript compilation.

But before that happens, it’s definitely a good idea to also run type-checking as part of your pre-commit git hooks.

To do this, I added a check npm script that runs type-checking (without emitting the compiled JavaScript).

package.json
"scripts": {
"dev": "cross-env TAILWIND_MODE=watch vite -d",
"build": "cross-env TAILWIND_MODE=build vite build",
"preview": "vite preview",
"check": "vue-tsc --noEmit"
}

Because I read the official docs, I knew to use vue-tsc rather than TypeScript’s typical tsc package. This enables us to apply TypeScript type-checking to .vue files as well as .ts files.

Now we make a hook in our .pre-commit-config.yaml to run our check script whenever there are new changes to a .ts, .tsx, or .vue file.

.pre-commit-config.yaml
repos:
...
- repo: local
hooks:
- id: typescript
require_serial: true
language: system
name: typescript
entry: npm run check
types_or: [ts, tsx, vue]
pass_filenames: false

Voila. Now pre-commit will run type-checking with every git commit.

Note that we must set pass_filenames: false otherwise our vue-tsc --noEmit command will only target the specific files that have changed. This is bad! That would exclude our own *.d.ts type definition files and every type definition file within our node_modules. That’ll cause your type checking to fail with the error: Cannot find module ... or its corresponding type declarations..

3. Defining type declartions for Vue components

Following the instructions in the docs allows you to use TypeScript within Vue SFC component scripts. But it doesn’t appear to give types to that SFC component itself. When you import your TypeScript-enabled SFC into a .ts file or another .vue file that has lang="ts" enabled, your IDE won’t recognize their types and the TypeScript compiler will complain. When you run your vue-tsc --noEmit you’ll run into an error like this:

Terminal window
django_vue_experiments/vite_assets/js/apps/001_vue_mvp.ts:4:25 - error TS2307: Cannot find module '@/js/components/DemoCounter.vue' or its corresponding type declarations.

The current standard for fixing this is sort of a catch-all solution. I have no idea where it originated, but it’s been co-signed by several stackoverflow answers, a Digital Ocean guide, and every real demo that I’ve come across.

In your src directory, you need to add this shim:

shims-vue.d.ts
/* eslint-disable */
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

This tells TypeScript that every vue component (exported from a "*.vue" file) has a type of DefineComponent<{}, {}, any>. This approach works, we can now import vue components into .ts files, but there are some problems with it.

It’s not terribly strict. Because the {} and any types are too loose we need to disable eslint checking on this declaration (Shout out to this real world demo). But most importantly, we lose the ability to get specific typing on each of our components. We can’t distinguish one component from another, and we can’t see the types for their props.

Prospective solutions:

Again, I’d love to hear from you if you’ve found a solution that works better.

I hope this MVP configuration was helpful. There’s definitely the potential to make the Vue component typing stronger, I’ll update this page as I learn more.

🔆

Further Reading