Vue.js with TypeScript
I spent a few days on and off learning parts of Vue to write a small app. I wrote the same app with Angular. I'm sharing my experience of working through Vue for the first time to help others that may be curious about the JavaScript framework landscape.
This post explores how I refactored the Vue app to use TypeScript. The two places I found the most help were in this (Microsoft resource](https://github.com/microsoft/typescript-vue-starter) and the Vue docs. Neither was exactly what I needed, but together they were helpful in gaining success. Read on to learn more.
Disclaimer: This was my first attempt at using TypeScript in Vue. My intent is to share the journey, not to claim this is the "best" way to use TypeScript. I know TypeScript well, but I am learning Vue and leaning on the experts from the Vue document and TypeScript team's resources on Vue for much of what I accomplished. I give credit to both of these teams for making these resources available and helping the community.
Why TypeScript?
Hold on a second ... Why? I mean, why use TypeScript at all? JavaScript is awesome, why do we need TypeScript? Well, we don't *need it. But I want it. But again ... why? Why do I want TypeScript? This is a question we should always ask when someone tells us we should use something. Far too often we find ourselves reading how to do something ... but the why is often far more important.
Simply put TypeScript allows me to code faster and to catch and rectify more problems at development time than without it. Intellisense, auto-complete, and great tooling are enabled when editors can use the TypeScript features. Need some examples? OK ... We can tell what the return type of an asynchronous function is an array of Hero models. Or we can find that we use a heroes
array in one place as an array and in another as an ES promise. When the editor is smarter, I can adapt quickly and fix problems before I deploy.
If you are interested in how I got started with Vue, here are some other posts that may interest you:
- Post 1 - File Structure
- Post 2 - Learning Materials
- Post 3 - Familiar Code
- Post 4 - Vue.js with TypeScript
Setup
I had to start somewhere, so I started by branching off of my connect2017
branch in this repo. This gave me a functional Vue app out of the box that I could begin refactoring to use with TypeScript.
I started by adding in the files that tell TypeScript how to do its job.
tsconfig.json
The first new file is tsconfig.json
. I started with the file that the Vue documentation recommends here. Then I enhanced it a bit. I found the changes to be super helpful.
The following file tells TypeScript to put the compiled JavaScript in the ./built
folder, to generate source maps for debugging and use TypeScript decorators (among other settings). I also added in lib
support for some core ES2016 features.
webpack.config.js
I knew I needed to change how the build process worked. For this, I opened the webpack.config.js
and immediately started to Google for some help. I find WebPack configuration to be painful ... hey, it's an awesome tool, but I admit that it's not easy for me to figure out what I need to modify in there. Luckily I found this guide by the TypeScript team that showed how to modify the file.
These are the git changes I made to my file.
We can see that the entry
file is now the main.ts
instead of main.js
. That makes sense. Then there are settings for a ts-loader
, which helps load the TypeScript with Vue. Finally, I added some extensions to the resolve
section.
Do I expect this is all perfect? Nope. It works ... and this is the kind of thing that once the Vue CLI adds a TypeScript template to its tooling, we won't have to mess with it at all. So let's proceed.
npm packages
I'm adding TypeScript to the project so it makes sense that I had to add some packages to support this. (The Vue CLI should handle this once it adds the new TypeScript template, too.)
First I ran npm install typescript ts-loader --save-dev
to add them as a development dependency. This came from the helpful Microsoft doc here. I deviated from that helped doc because I already had some of the others and I like to code dangerously.
Next, I ran npm install vue-property-decorator vue-class-component --save
to install support for the TypeScript decorators. The decorators will help define classes as Vue components and define props
.
Note: The @Component()
decorator is in vue-class-component
but it is imported and re-exported from vue-property-decorator
. So you can import from both of these, or just the latter. But you will want to npm install
both.
vue.shims.d.ts
We can define files that help TypeScript and the tooling know how to handle some specific types. The helpful Microsoft doc here suggested adding the following code to a new file named vue.shims.d.ts
. It basically makes it easier for the tooling to know how to handle *.vue
files, also known as Single File Components (SFC).
Refactoring the Code Files
A great way to look at the changes required during my refactor is to use a file by file comparison. Let's begin.
main.js --> main.ts
I renamed the main.js
file to main.ts
. Yep, no code changes in this one. Move along.
App.vue
The App
component is the root component in the Vue app. Here is its original file using Babel with JavaScript.
The next image shows the TypeScript. Take a glance, then read on to learn how I refactored the code to get here.
First, I added a hint to the <script>
tag to let it know I will be using TypeScript. Notice the tag is now <script lang="ts">
in the image below.
I added the @Component()
decorator to both tell Vue that the class following the decorator is indeed a Vue component and that this component will reference a child component named HeroList
. This components
property in the decorator was lifted right from the components
property in the JavaScript example.
I refactored the default export to become a named class App
that extends Vue
. This makes sense as the name of our component is App
. We extend Vue
as a way of preparing our class to be a component. See an example in the Vue docs here.
I refactored the data
properties to become public class properties. Then I initialized the title property in the constructor. I could also have done this in a lifecycle event, which probably makes more sense.
Notice I added a few import
statements to the top of the file. This helped the tooling and TypeScript know where to get the Vue
and Component
symbols.
HeroList.vue
The HeroList.vue
file contains the logic for getting and displaying the heroes. The JavaScript version is shown below for reference. Notice I collapsed the components
and methods
to make it easier to follow. Full source code can be found at the end of this post.
Here follows the TypeScript version of the file, after my refactoring effort. Take a moment to see the differences and how they map to each other.
Just as with the App.vue
file, the components
moved up to the decorator and the data
properties became public class properties. There are some subtle differences here though as we look closer. Notice that I added explicit types to selectedHero
and heroes
. I did not need to do this, but it helped clarify things.
The selectedHero
was initialized to null
so the details wouldn't appear until a selection had been mad. But now that I have a Hero
type
(we'll explore that in the next section), I wanted to be clear to the compiler of my intentions that the selectedHero
should be allowed to be null
or of type Hero
.
But perhaps far more interesting is how I went from initializing the heroes
array right in the data function in the JavaScript example to initializing it in the created
life cycle event in the TypeScript example. This is one place I found a bug in the JavaScript example. I was setting the heroes to the return value from the getHeroes
function ... which was not an array of heroes, but instead a promise
of an array of heroes. This did not bite me in the running code because I was, but it could have. It worked only because the getHeroes
function sets the heroes
array ... so it immediately ran and filled the array anyway. But this just hid the bug. When I refactored it to use TypeScript the editor alerted me that I had a type mismatch. Then I moved the call to getHeroes
to the created
lifecycle event. Ladies and gentlemen, that's a real bug ... not something I staged. Oops! But thankfully the editor caught my mistake and I was able to correct it during the refactor.
We also have some new imports up top in the TypeScript file and I added some more explicit typings in a few places, so I could make sure I had no more hidden bugs. That's the value of setting noImplicityAny: true
in the tsconfig.json
file.
Hero Model
Now that I have types, I added a hero.ts
file to define a Hero
class. This helped make sure the components that deal with heroes are using the right types and properties.
HeroDetail.vue
The HeroDetail
component had many of the same changes as the HeroList
component. Some imports up top, the @Component()
decorator, moving the data
properties to public class properties, and defining some explicit types.
Check out the JavaScript version here.
Now take a glance at the TypeScript version, after the refactoring effort.
What's new here is that I am using the @Props()
, @Watch()
, and @Emit()
decorators in the TypeScript version. Notice the hero
that I was watching in the JavaScript version is now a public class property decorated with @Prop()
. This was a simple refactoring.
Then I defined a named function onHeroChanged
and decorated it with the @Watch()
decorator. This went from a function named hero
in the JavaScript version to a named function with a @Watch()
decorator in the TypeScript version.
I converted the code where I emitted events using this.$emit
in JavaScript to now use the @Emit()
decorator. This decorator accepts the name of the event being emitted. The receiving function in the HeroList
component also was slighlty refactored, since now I am emitting 2 arguments instead of one. Here is an example of how I refactored these emitters:
@Emit('unselect')
clear() {
this.editingHero = null;
}
@Emit('heroChanged')
emitRefresh(mode: string, hero: Hero) {
this.clear();
}
I access the $refs
to grab the elements to set focus on them. I made a public class property named $refs
and typed it with the names of the two references I had in the Vue template. Both are of type HTMLElement
.
Hero Service
The hero service refactor was quite simple. The TypeScript version became an instance of a class that I could import into the components and use as needed. Nothing out of the ordinary here. This could have simply been a set of exported functions, too. There are lots of ways to handle services in JavaScript/TypeScript.
Summary
The best question to ask is "was it worth it?". Only you can decide. I think it was worth it. It caught a bug I had in the JavaScript version and now I get a lot more help from my tools, like VS Code, when writing the app.
I'm looking forward to the official TypeScript template for the Vue CLI. But in the meantime, there are a few ways to use TypeScript with Vue projects.
Source Code
You can find the source code for these apps here:
- Angular / TypeScript app - in the
vue-similar
branch - Vue / JavaScript app - in the
connect2017
branch - Vue / TypeScript app - in the
typescript
branch