Last year I was presented with a decently sized vue project, build by mostly new developers, which I was tasked to refactor.
First I had to get to know what the application actually did. I spun up a new instance and was greeted by a deep red developer console.
Oh. Yeah. We don’t know quite yet where those errors come from, but it works!
And it did work, well kind of, which was pretty amazing to be honest. But I didn’t have to wait too long for bugs and slowdowns to appear.
In my opinion the first step was to introduce good version control. I didn’t just want to refactor the application itself, but I saw it more of a refactoring of the project as a whole. A lot of this will seem basic knowledge for a lot of developers. But if you start out fresh you seem to forget or not care about this stuff. You just want to start programming and get things done. We all heard it before:
“Before you even start to write code, think about what problem you are trying to solve and how you think you can achieve that”.
Many new developers don’t take this advice serious enough or forget that this advice should also be considered for things which don’t involve actual coding. Like Project management.
GIT was already in use, which I welcomed very much. How they used it however was not ideal. Everybody would just push to one branch. Sometimes they would create a new branch to deploy a certain version. Often times fixes to this new branch would not be merged in to the original development branch leaving behind recurring bugs when it was time to branch out a new version. Another Issue was formatting. Everyone had different formatting rules configured. This would lead to massive pushes just because one developer would convert tabs to spaces, append semicolons and use Camel Case. Just for the next developer to generate another huge push reversing those changes.
- Choose a branching strategy and stick with it.
- Choose uniform formatting.
- Check your commits before committing them.
- Use merge requests.
No really! If you anticipate your application to grow, if you work with other people, or even if you plan to maintain your application for a longer period of time: Invest time in a good version management!
After a little workshop we decided on a branch strategy, I configured eslint and prettier to use uniform formatting, and we introduced merge requests. Fine right? Not quite. Even though I briefly mentioned rebasing, I didn’t put enough emphasis on it. After a few months, a new feature was ready to be merged. In those few months a lot of changes happened to the original branch, including restructuring. You can imagine what the merge request looked like. It was huge. The merge took a long time, and we even hit a point where the only feasible option was to do it all over again because we lost the overview.
If your feature branch gets very old, rebasing is a good tool to avoid massive merge conflicts
In my experience you should still be careful with rebasing though. Merging is my preferred way to introduce new features, but when you have such a huge reconstruction of your codebase the merge conflicts just get too much to handle, so rebasing smaller changes from time to time are easier to handle.
Cannot read property ‘x’ of undefined
Next step: Actually looking at the code itself and introducing Typescript. Why Typescript?
I would often encounter object definitions on top of a function. You would assume this describes the type, but you can never be sure. Nothing prevents the developer from just adding new properties way later. Combine that with no merge requests and code reviews because the code “works”, and you are left with a codebase which is unreasonable hard to follow.
Another recurring issue was “numbers” and casting. You never knew if you are currently handling a number or a string representation of a number. This would lead to either redundant checks, ‘not a function’ errors or in the worst case wrong type casting.
Imagine you are working with what you assume is a number and you add another variable you also assume to be a number. If 5 + 9 equals 59, you know you should start to care about data types.
Null checking. At first glance exemplary. But again, not exactly knowing the type you are working with, combined with a lot of convoluted type conversion is not that easy to handle for new programmers. Null checking a property of an object which doesn’t exist with ‘!== null’ lands you in trouble! Just use the property itself inside the if condition. It will create a new Boolean out of the property which in turn returns the truthiness of said type according to the documentation.
If you need to care about types anyways why not just be more explicit about them. Let a type checker make sure you are working with the right type. Invest a bit more time and get a huge return. So I went ahead and made plans to convert the codebase to typescript. But how do you even start?
To expand on this style I choose vuex-module-decorators for the vuex modules.
What do you actually need for the transition to TypeScript? It turns out not that much.
- tsconfig.json: The configuration file for your TypeScript compiler. The Vue documentation has you covered here.
- webpack: Again if you just follow the documentation on how to use typescript with webpack and you are good to go.
- @type/mypackage: Most packages already export type definitions. You can easily install them with npm/yarn.
- vue-shims.d.ts: If you are using single file components you need to tell the compiler how to handle those files.
Converting Vue components was pretty straight forward. Instead of exporting a function you export a class which extends on the vue class provided by vue-property-decoratos. Data members represent the state and function members the actions.
Next step annotating types. I found it easier to define one big interface and let the class implement said interface instead of defining types for each data member.
Just like the components you can export a class which extends a vuex module and create annotations.
Here we would encounter the true benefit of types for the first time. The store actions would actually take a specific type of parameter defined beforehand. So you just could not pass wrong data from your component to this action. You also didn’t have to look up the source of the store action to know what type of parameter exactly this action expects.
A huge amount of undefined property uses, wrong type conversion and even redundant code was fixed by simply defining types.
This saves so much time. Imagine the time you lose debugging undefined and null issues. Combine that with an unreadable codebase and you are in for some fun!
To complete the refactoring we created type definitions for every API call. It included a response type which itself contained a generic data property. Every response would return an object of this type and would fill the generic with an object described again by its own type. The same goes for the request.
All in all it took a few weeks to refactor the code. Was it worth it? Completely! The amount of bugs were reduced by a huge amount, code quality went up dramatically and additions to the code were way easier. I felt like the TypeScript compiler would lead me to the things which had to be refactored instead of me searching aimlessly.