Many Node.js projects will reach a level of complexity where we'll often find ourselves wondering what a certain object's structure looks like. We'll also have the need for early warnings about errors due to trying to access fields that don't exist or assuming different types from what they really are. These are good indications that you would derive a lot of benefit from using a type-checking system such as TypeScript. This article will look into how to make the transition from JavaScript to TypeScript in an existing project.
TypeScript-support for Node.js is provided by the typescript
NPM package. The best way to implement this package will depend on your project's build tooling. If you have webpack set up, then the easiest way will be by using the ts-loader
package (see the "Setting up ts-loader for webpack" section below for this ts-loader
+ typescript
setup). However, if you don't have a module bundler configured, then the simplest way to add TypeScript will be through the tsc
("TypeScript compiler") command, which is included in the typescript
package. This tsc
setup is described next.
Setting up TypeScript compiler (tsc)
tsc
is the official TypeScript transpiler which converts your TypeScript source files into JavaScript files that can be executed by Node.js or the browser. This section assumes you have a working Node.js project where you run your main js-file directly using node
.
The first order of business is adding the typescript
package to your project as a dev-dependency:
npm install --save-dev typescript
The next step is to create a config file for typescript
. A good starting point is to generate the file using the command npx tsc --init
. Add the following properties to the compilerOptions
property in the generated file:
-
"noImplicitAny": true
— Disallows the use of theany
type - a common anti-pattern in TypeScript. -
"allowJs": true
— Allows us to have JavaScript (.js) files amongst the TypeScript (.ts) files. When we need to migrate an existing JavaScript project to TypeScript, this allows us to systematically convert files from JavaScript to TypeScript one at a time. Once the conversion is complete, this flag can be removed. -
"outDir": "dist"
— The folder where the transpiled JavaScript files will be placed. -
"rootDir": "src"
— The location of your TypeScript / JavaScript source code.
After adding these properties the tsconfig.json looks as follows:
And just like that, the code is ready to be compiled! Just run the command npx tsc
and see the output files appear in the dist folder.
Before trying to run the compiled code, keep in mind that tsc outputs CommonJS-style JavaScript. This means that if your source code is written as ES modules, you need to change the "type"
property in your package.json from "module"
to "commonjs"
in order to run the compiled code (tsc will still interpret your source code as ES modules). At this point the "main" .js file in the dist folder should run successfully via the node
command: node dist/my-app.js
Source maps
A side-effect of running the compiled code instead of running the source code directly is that stack traces of errors will refer to the line numbers inside the compiled code instead of in the source code, which is not very helpful. Luckily we can have tsc generate source map files which map each line in the compiled code to the corresponding line in the source code. These can be used to make our application report the correct line numbers in error stack traces.
Getting tsc to generate the source map files is an easy matter of adding the "sourceMap": true
property to the "compilerOptions"
in tsconfig.json:
Run npx tsc
again and note that in the dist folder a .js.map file is created for each compiled .js file. However, we still need to make these mappings be interpreted at run-time. To do this, add the source-map-support
package as a run-time dependency. We also add its types declaration package for TypeScript as a dev-dependency:
npm install --save source-map-support
npm install --save-dev @types/source-map-support
And activate it by adding the following to your main source file:
Compile and run the application. Error stack traces will now refer to the lines in the source code.
Adapting script commands
Creating script commands (in package.json) for compiling and running the application is pretty simple:
"build": "tsc",
"run": "node dist/my-app.js",
For a streamlined developer experience we would want to have a command that will listen for source file changes and then recompile and restart the application whenever they occur.
The tsc
command conveniently has a --watch
flag we can use to recompile. Then we can use the nodemon
package to restart the application whenever we detect file changes in the dist folder (due to the recompilation). Hence we can have the following two scripts:
"build:watch": "tsc --watch",
"run:watch": "nodemon dist/my-app.js --watch dist",
But we need these two commands to run at the same time. This can be achieved using the npm-run-all
package.
Add the required packages as dev-dependencies:
npm install --save-dev nodemon npm-run-all
Final list of scripts:
"build": "tsc",
"run": "node dist/my-app.js",
"build:watch": "tsc --watch",
"run:watch": "nodemon dist/my-app.js --watch dist",
"start": "npm-run-all --parallel build:watch run:watch"
Run npm start
to compile and run the application. Whenever you make a change to a source file, the application will automatically recompile and then restart.
Setting up ts-loader for webpack
If your application already has build tooling set up via webpack, the easiest way to add TypeScript support is by using the ts-loader
package.
This time around, add the ts-loader
and typescript
packages as dev-dependencies:
npm install --save-dev ts-loader typescript
The same tsconfig.json configuration file as above can be used in this case:
Note that in this case, since webpack is already configured to process JavaScript source files, there is no need for including the "allowJs": true
flag here, unless you would like both JavaScript and TypeScript files to be processed by ts-loader. If this is the case, make sure to include the extension js
in the "test" property of the rule added to webpack.config.js below.
In webpack.config.js add a rule telling webpack to invoke ts-loader for all TypeScript files:
At this point the application should build and run fine. We are now ready to start converting .js files to .ts files.
Migrating existing JavaScript files to TypeScript
At this point we should have a tooling setup capable of compiling a combination of JavaScript and TypeScript files. This means we can systematically convert JavaScript files to TypeScript one at a time, compiling and testing the application along the way by renaming a .js file to .ts and fixing the compiler errors as they come up. Once all .js files in the project have been converted, the "allowJs": true
flag in the tsconfig.json can be removed.
Here are some general notes to observe during this conversion:
use strict
Any 'use strict'
directives in existing JavaScript files can be removed since the "strict": true
setting in the tsconfig.json causes 'use strict'
to be generated into the compiled JavaScript files automatically.
Extending the Error class
If you have defined any sub-classes of Error
, note that there is a known bug in TypeScript whereby testing for an instance of this error using instanceof
will not work.
See this StackOverflow post for a work-around. If you have multiple sub-classes of Error
, I would recommend applying the work-around to a common "base" error class (eg. class ErrorBase extends Error
) and have all other error classes extend this class.
Alternatively, if your code need not support running on IE 11, you should be able to safely change the compiler target from ES5 to ES6 by changing the "target"
property in tsconfig.json to "es6"
(see the ES6 compatibility chart). This way tsc will generate all classes as actual ES-classes in the target code, effectively circumventing the bug and obviating the need for the work-around.
Conclusion
There are many benefits to TypeScript that make it worthwhile to take the time to set it up for new projects, and even to convert from JavaScript in existing projects. Making the necessary changes to an existing project's build tooling is generally quite simple, and for new projects there is no need to add a module bundler just to be able to use TypeScript, thanks to tsc.
I have applied this migration to a project from my other articles. Feel free to view the version with the tsc setup or the version with the webpack / ts-loader setup on GitHub.
Top comments (3)
Nice post 👍
It can be a lot easier than that, just depends on you working on OOP or not. check this out 😄
I was actually searching something like this!
Thanks ✌️