Make your legacy app great again?
Introduction
Why would you do that ?
A developer?s job is rarely only working on edge technologies and starting every 2 months a new project from scratch (it?s what you do ? then keep that job, it?s the dream job of everyone ?). A lot of a developer?s job is maintaining legacy application because they still bring money home, but perhaps not enough to be rewritten every 5 years. Sometimes it?s the technology that?s old, lot of time it?s the whole code base that is creepy (I already hear you yelling at code authors about that shitty implementation even if you was the author ?). So when you go back to an old project, it?s good to take some time for quick wins that will help you later. Using Typescript is one of them. And we?ll see here how to make this migration.
Photo by Dietmar Becker on Unsplash
My actual application technology
- angularJS 1.7
- Several libraries (in fact much more than you need?) like angular-ui-bootstrap, fullcalendar, d3js, jquery, underscore, moment.js, etc.
- A grunt build system that transpile with babel and then concat/uglify the result (no webpack or browserify)
- Karma + jasmine for unit tests
1. Setup Webpack
Typescript supports module and it?s something you wan?t in your app. That?s why you need webpack. You could choose to use browserify, but in my opinion Webpack is lot more performant and you won?t need a taskrunner aside.
Setup the config
You want to have webpack working, so you need 2 things :
- Install dependencies npm i -D webpack webpack-cli
- Setup the config webpack.config.js. Fortunatelly, for that part, you don?t need to write your own file from scratch. You will find plenty of generators like createapp.dev to do the work for you. Just choose the option you need.
Import all js files in a root file
Once webpack is working, you will have to specify which file is the entry point. The problem is that if you?re setting up webpack there is big chances that you have no ESModules in your code, and that you won?t have a single entry file. The solution is to create an index.js file at your application root level, and you will import manually all the files.
// index.jsimport ‘./feature1/feature1.controller.jsimport ‘./feature2/feature2.controller.js// and so on for all your files in the right order…
Not that you could also define multiple files as the webpack entry, but I personally prefer having a list of imports in the source code instead of a list of files in the build.
Check that the application still works
If not, fix everything now. You have probably missing imports or the order is wrong or whatever. Fixing the app now will make the next chapter easier.
Fix the unit tests
This was my biggest pain as we add already a bunch of tests for the JS part. The tests were testing some global functions and also angular modules. As now the code was partially migrated to imports, the tests were not able to find former global function as they were scoped in the modules of the webpack wrapper. It seems (and I hope I?m wrong but wasn?t able to find out a solution) that you will have to either migrate everything to module or also declare some import function as global to let the test run correctly.
import { otherDependencies } from ?./otherDependency?;export function helloWorld() { ? }window.helloWorld = helloWorld; // declare the function globally
It?s not clean at all we all agree, but it?s the only way I could start the module integration without refactoring everything. Once the Tests are running again and the TS setup is working, you could take time to do this refactoring to a clean import code base.
2. Setup TypeScript
Install typescript
npm install -D typescript
Add ts files handling in webpack
module.exports = { … module: { rules: [ …, { test: /.tsx?$/, use: ‘ts-loader’, exclude: /node_modules/, } ] }, resolve: { extensions: [‘.tsx’, ‘.ts’], },};
Create a tsconfig.json
TypeScript will need a basic config to compile the way you want. You will have all the information you need on their documentation : https://www.typescriptlang.org/docs/handbook/tsconfig-json.html
In my case, I?ve juste added this simple config as a first step
{ “compilerOptions”: { “outDir”: “./dist/”, “noImplicitAny”: false, “module”: “es6”, “target”: “es5”, “allowJs”: true, “lib”: [ “dom”, “es7” ] }}
noImplicitAny is set to false for now. It?s not recommended to keep that setting disabled because you should not type your code with any . But for now, we don?t want to change the code as you could introduce new bugs. We just want to have a working compile task and it?s already enough work. Later, the next improvement will be to enable that settings, and fix all implicit any.
lib with es7 will add functions that are parts of the es7 specification to the TypeScript checks. As an example, you won?t have errors when using string.includes which is not available before es7. I?ve chosen es7 as I was already using ESNext with babel in my JS, but you should set here a value that match your language level.
Add types for globals
If you try to run webpack to test the compilation, the TS compiler will probably yell at you because it doesn?t recognize globals like angular. In that case, you will have to teach TS what types are those constants. For most of the libraries, nice people have done typing files *.d.ts which already contains all the types of the library. Just install them to make TypeScript aware of their typings. In my case to inform about angular and jquery, I installed the following :
npm i @types/angular @types/jquery
You won?t have to do any other configuration. By default, TypeScript will look at all types available in the folder : node_modules/@types and that?s where the angular type was installed by npm.
Fix errors
At this step, you?ll have to run the compilation to see what errors are resulting. As for now we just want to setup TypeScript in our code base, and not rewrite the whole, we?ll see how we can quickly fix the common errors you could have. It?s not the best practice, neither the final implementation, you?ll have to clean this in later ticket. But for now, let just make that code base compile.
If you have a huge amount of errors about the any type, first check that you?ve added “noImplicitAny”: false, in the tsconfig.json like described above.
If you have other errors, it?s perhaps the same one I add and you can look the solution here below.
Fix variable declarations
Some of the variables in the code were initialized as an object and the object members were added to the object later in the code.
var foo = {};foo.bar = ‘bar’;foo.baz = function(){ return ‘baz’; }
But TypeScript don?t like that because you first declare it as an object and then try to change that type by adding properties. The solution is to refactor that code to use a correct object initialization. Like following :
var foo = { bar: ‘bar’, baz: function(){ return ‘baz’; }};
Fix scope extension
In angularJs, the scope of a component (or directive) can be extended with custom properties we want to use in the component. But the IScope interface from the angular type delcaration only knows the base properties.
To fix that, you will have to create your custom interface that extends IScope and contains your properties. Ex:
interface MyComponentScope extends angular.IScope { myProp1: number; // … and so on for all your properties+types}// and then type the scope param like :link: function (scope: MyComponentScope, …) { … }
Fix bindings
When using components in angularJS 1.5+, there is a new (it was new some years ago yes?) property called bindings that binds to the controller. Like the scope extension, those bindings are custom and you will need to define their types. Here is an example of an existing binding declaration:
class MyComponentCtrl { // existing members and methods here //…}angular.module(‘my-module’).component(‘myComponent’, { templateUrl: ‘path/to/my-component.component.html’, controller: MyComponentCtrl, bindings: { text: ‘@’, resolve: ‘<‘, close: ‘&’, dismiss: ‘&’ }});
So now just add the missing bindings properties in the class
class MyComponentCtrl { // from the bindings text: string; resolve: { title: string, items: any }; close: (obj: { $value: any}) => void; dismiss: () => void; // existing members and methods here //…}
Fix the unit tests
You already did it during the Webpack setup. But trust me, you probably have broken something in between so run the tests again and fix them.
3. Improve the whole thing
Now that the setup is running, building, testing, there is still a lot of work to do to really use the full power of the imports, but the remaining one can be done along with other new tasks :
- convert existing code to es6 class and imports
- import used libs components only instead of full libs
- add typings everywhere
- Remove all any
- Enforce typings by restricting the use of the any type (configs like noImplicitAny)
- Remove libraries that were juste used for features that are now part of ESNext (underscore, jquery,? )
- Remove global declarations and import modules instead
- Unit test the modules not the app
Conclusion
It took me 1 working week to do this switch to Typescript. You could probably achieve it in lot less time as I had to google and learn a lot about the tools to make things work correctly. Also I earned the codebase 2 days before so it was not a well known app where you can anticipate surprises.
Anyway switching to Typescript without refactoring had already some benefits. It already discovered several bugs about wrong method parameters, wrong function calls,? and will prevent further one (even if we know it?s not bulletproof).
Hope this article could help you too.