Adding TypeScript-support to your Node.js project
Tags: node, typescript, javascript, tutorial
Published: 2021-11-15 11:18:08

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` &mdash; Disallows the use of the `any` type - a common anti-pattern in TypeScript.
- `"allowJs": true` &mdash; 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"` &mdash; The folder where the transpiled JavaScript files will be placed.
- `"rootDir": "src"` &mdash; The location of your TypeScript / JavaScript source code.

After adding these properties the *tsconfig.json* looks as follows:

{% gist https://gist.github.com/bhaeussermann/c59070b33fbbcec1fc5a7ec517e99d4a %}

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*:

{% gist https://gist.github.com/bhaeussermann/bff8c7ad5a11d0c5c6bd66d0019471f3 %}

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:

{% gist https://gist.github.com/bhaeussermann/b35432581c3599f13b91b61f0fb7160e %}

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:

{% gist https://gist.github.com/bhaeussermann/0b51151e890fb3d79429e20c005de2d7 %}

*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:

{% gist https://gist.github.com/bhaeussermann/9293079193804e8289c4ac57c071defb %}

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](https://github.com/microsoft/TypeScript/issues/13965) in TypeScript whereby testing for an instance of this error using `instanceof` will not work.

See this [StackOverflow post](https://stackoverflow.com/a/48342359/359765) 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](https://kangax.github.io/compat-table/es6)). 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](https://github.com/bhaeussermann/node-js-rest-api/tree/typescript-tsc) or the version with the [webpack / ts-loader setup](https://github.com/bhaeussermann/node-js-rest-api/tree/typescript-webpack) on GitHub.*