How to Set Up a Node.js Project with TypeScript
Tags: node, typescript
Published: 2022-01-26 13:18:00

In this tutorial, you will learn how to add TypeScript support to Node.js projects. We will address common needs, such as:

- compiling and executing the code
- debugging the source files
- configuring third-party packages so that the TypeScript compiler also validates them

Let's get going!

## Why Use TypeScript?

[TypeScript](https://www.typescriptlang.org/) brings optional static typing to JavaScript projects. The primary benefit of static typing is that type errors are detected and corrected at build time, so code will more likely run correctly once deployed to production. [A growing number of JavaScript developers](https://2020.stateofjs.com/en-US/technologies/javascript-flavors/) are seeing the value of writing more strongly typed code, which has led to an increase in the adoption of TypeScript for all kinds of JavaScript-based projects.

## Prerequisites

This article assumes that you have a basic knowledge of [Node.js](https://nodejs.org/) and TypeScript. You also need to have a recent version of Node.js and npm installed on your computer.

## Installing and Configuring TypeScript on Node.js

Besides the web browser, Node.js is the most popular platform on which JavaScript code is executed. However, just like the browser, it lacks native support for TypeScript code (unlike [Deno](https://deno.land/), for example).

Before installing TypeScript, make sure that you've created and initialized a Node.js project with a `package.json` file. TypeScript is available as a package on the [npm registry](https://www.npmjs.com/package/typescript), and it can be downloaded into your project through a package manager like [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/):

```bash
$ npm install typescript --save-dev
```

Once the above command succeeds, you can check the current version through the following command:

```bash
$ npx tsc --version
Version 4.4.3
```

It's advisable to install TypeScript as a project-specific dependency. The same version of the language will then be used, regardless of the machine running the code. You can also install the TypeScript CLI globally by using the `--global` switch. A global installation can come in handy for running one-off scripts for testing purposes.

```bash
$ npm install typescript --global
$ tsc --version
Version 4.4.3
```

If you use a Node.js environment manager like [Volta](https://volta.sh/), you'll be able to switch between globally installed TypeScript versions and project-specific ones seamlessly.

Now that TypeScript is installed in your project, create a [configuration file](https://www.typescriptlang.org/tsconfig/) that specifies which files should be compiled and the compiler options for the project. This file is called `tsconfig.json`, and you should place it at the root of your project directory.

```bash
$ touch tsconfig.json
```

Here's a basic configuration that you can start with:

```json
{
  "extends": "@tsconfig/node16/tsconfig.json",
  "compilerOptions": {},
  "include": ["src"],
  "exclude": ["node_modules"]
}
```

The above configuration file extends the [base configuration](https://github.com/tsconfig/bases/) provided by the TypeScript team for Node.js v16. [Additional options](https://www.typescriptlang.org/tsconfig#compilerOptions) or overrides may be included through the `compilerOptions` property. It also specifies that all the files in the `src` directory should be included in the program, but everything in the `node_modules` directory is skipped entirely. Both the `include` and `exclude` properties support [glob patterns](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#glob-support-in-tsconfigjson).

Before you proceed, ensure you add the base configuration package for Node.js v16 to your project's `devDependencies` through the command below. Base `tsconfig.json` packages also exist for [Node 10](https://www.npmjs.com/package/@tsconfig/node10), [Node 12](https://www.npmjs.com/package/@tsconfig/node12), and [Node 14](https://www.npmjs.com/package/@tsconfig/node14) at the time of writing.

```bash
$ npm install @tsconfig/node16 --save-dev
```

## Compiling TypeScript Files for Node.js

Go ahead and create the aforementioned `src` directory in your project root, and place a `main.ts` file inside it. This file should contain the following code:

```typescript
function sayMyName(name: string): void {
  if (name === "Heisenberg") {
    console.log("You're right 👍");
  } else {
    console.log("You're wrong 👎");
  }
}

sayMyName("Heisenberg");
```

Save the file, then attempt to compile the TypeScript code to JavaScript
through the command below:

```bash
$ npx tsc
```

You will get an error indicating that the compiler does not understand the `console` object:

```
src/main.ts:3:5 - error TS2584: Cannot find name 'console'. Do you need to change your target library? Try changing the 'lib' compiler option to include 'dom'.
src/main.ts:5:1 - error TS2584: Cannot find name 'console'. Do you need to change your target library? Try changing the 'lib' compiler option to include 'dom'.

5 console.log(product(10, 5));
  ~~~~~~~

Found 1 error.

3     console.log("You're right 👍");
      `~~~~~~~`

src/main.ts:5:5 - error TS2584: Cannot find name 'console'. Do you need to change your target library? Try changing the 'lib' compiler option to include 'dom'.

5     console.log("You're wrong 👎");
      `~~~~~~~`


Found 2 errors.
```

This error occurs because the `lib` compiler option set in the [base configuration](https://www.npmjs.com/package/@tsconfig/node16) for Node.js v16 does not include the `dom` option, which contains type definitions for the `console` object and other browser-specific APIs. The error message above suggests adding the `dom` option to the `lib` property to fix the problem, but this is not the correct solution for a Node.js project. The correct fix involves installing the [type definitions for Node.js APIs](https://www.npmjs.com/package/@types/node) so that the TypeScript compiler can understand and validate all the built-in Node.js APIs. 

Here's how:

```bash
$ npm install @types/node --save-dev
```

Once installed, the error will vanish the next time `npx tsc` is run and a `main.js` file will be produced in the `src` folder with the following content:

```javascript
"use strict";
function sayMyName(name) {
    if (name === "Heisenberg") {
        console.log("You're right 👍");
    }
    else {
        console.log("You're wrong 👎");
    }
}
sayMyName("Heisenberg");
```

You can subsequently execute this file through the `node` command:

```bash
$ node src/main.js
You're right 👍
```

If you want to change the folder where the JavaScript files are placed, you can use the [outDir](https://www.typescriptlang.org/tsconfig#Output_Formatting_6256) compiler option in your `tsconfig.json` file:

```json
{
  "compilerOptions": {
    "outDir": "dist"
  }
}
```

Subsequent compilations will emit the JavaScript output into a `dist` folder.

```bash
.
├── dist
│   └── main.js
├── package.json
├── package-lock.json
├── src
│   └── main.ts
└── tsconfig.json
```

## Execute TypeScript Source Files Directly With ts-node

The process of compiling TypeScript source files into JavaScript code before executing them with Node.js can get a little tedious after a while, especially during development. You can eliminate the intermediate steps before running the program through the [ts-node](https://github.com/TypeStrong/ts-node) CLI to execute `.ts` files directly. Go ahead and install the `ts-node` package using the command below:

```bash
$ npm install ts-node --save-dev
```

Afterwards, execute the `main.ts` file with the `ts-node` command:

```bash
$ npx ts-node src/main.ts
You're right 👍
```

Using `ts-node` in this way places the TypeScript compiler as a middleman between the source files and the Node.js runtime. It transpiles the source code before executing the resulting JavaScript code with `node` (performed under the hood). This makes the script execution a bit slower than JavaScript output directly with `node`. You can opt out of type checking through the `--transpile-only` or `-T` flag to make the script execute faster in scenarios where type validation isn't essential.

Another feature that `ts-node` enables is the transformation of modern `import` syntax to CommonJS syntax. This means that when using `ts-node`, you can use `import` instead of `require` to utilize Node.js modules in your code.

[Learn more about this feature in the `ts-node` project's README document](https://github.com/TypeStrong/ts-node#commonjs-vs-native-ecmascript-modules).

## TypeScript Integration with Third-party Node.js NPM Packages

When utilizing Node.js packages from the npm registry, additional setup may be required to compile the project successfully.

The main problem is that most of the packages you're likely to encounter are written in vanilla JavaScript, so TypeScript cannot determine the valid types for exposed methods. Everything from the library is implicitly typed as `any`.

Here's an example that utilizes the popular [Express package](https://www.npmjs.com/package/express) to create a web server:

```typescript
import express from 'express';
const app = express();

app.get('/', function (req, res) {
  res.send('Hello World');
})

app.listen(3000);
```

Assuming you've installed the express module with `npm install express`, execute the script with `ts-node`. It should yield the following errors:

```bash
$ npx ts-node src/main.ts
TSError: ⨯ Unable to compile TypeScript:
src/main.ts:4:24 - error TS7006: Parameter 'req' implicitly has an 'any' type.

4 app.get('/', function (req, res) {
                         ~~~

src/main.ts:4:29 - error TS7006: Parameter 'res' implicitly has an 'any' type.

4 app.get('/', function (req, res) {
                              ~~~


Found 2 errors.
```

The TypeScript compiler is responsible for the errors shown above. It cannot determine the type of the `req` and `res` parameters in the callback function, so they are both implicitly typed as `any`.

Since the `strict` compiler option is set to `true` in the [base tsconfig.json](https://www.npmjs.com/package/@tsconfig/node16) file, the [noImplicitAny](https://www.typescriptlang.org/tsconfig#noImplicitAny) compiler option is also enabled. This ensures that TypeScript will emit an error instead of inferring type `any` when it is unable to determine the type of a value.

You can fix this error by providing a type declaration file for the `express` module so that the TypeScript compiler can accurately determine the valid types for its exported methods. You can find up-to-date type definitions for many popular npm packages in the [DefinitelyTyped GitHub repository](https://github.com/DefinitelyTyped/DefinitelyTyped).

The type definition for a package can be downloaded through the `@types` scope. For example, use the command below to install the [type definitions for Express](https://www.npmjs.com/package/@types/express):

```bash
$ npm install @types/express --save-dev
```

After installing the type definitions for Express, compiling and executing the script with `ts-node` should complete successfully without errors.

## Linting TypeScript with ESLint

An important step towards adding comprehensive support for TypeScript in a Node.js application is setting an adequate linting workflow. You can make use of the popular [ESLint](https://eslint.org/) package to lint TypeScript code. Although it was originally written for JavaScript code, it also supports TypeScript with the help of a few plugins. Go ahead and install the [eslint package](https://www.npmjs.com/package/eslint) in your project with the command below:

```bash
$ npm install eslint --save-dev
```

Now create a new `.eslintrc.js` file in the root of your project directory. Here is where you'll place the [configuration settings](https://eslint.org/docs/user-guide/configuring/configuration-files) for ESLint. To add TypeScript support to ESLint, install the [@typescript-eslint/parser](https://www.npmjs.com/package/@typescript-eslint/parser) and [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin) packages in your project. The former is used to parse TypeScript code to a format that is understandable by ESLint, while the latter provides TypeScript-specific linting rules.

```bash
$ npm install @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
```

Once both packages are installed, open up your `.eslintrc.js` file in your editor, and enter the following:

```javascript
module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest', // Allows the use of modern ECMAScript features
    sourceType: 'module', // Allows for the use of imports
  },
  extends: ['plugin:@typescript-eslint/recommended'], // Uses the linting rules from @typescript-eslint/eslint-plugin
  env: {
    node: true, // Enable Node.js global variables
  },
};
```

If you want to override any of the [linting rules](https://eslint.org/docs/rules/) or configure other rules, use the `rules` property in the `.eslintrc.js` file, as shown below:

```javascript
module.exports = {
  . . .
  rules: {
    'no-console': 'off',
    'import/prefer-default-export': 'off',
    '@typescript-eslint/no-unused-vars': 'warn',
  },
};
```

After saving the file, run the ESLint CLI on your TypeScript code through this command:

```bash
$ npx eslint . --fix
```

You can also create a `lint` script in your `package.json` file as follows:

```json
{
  "scripts": {
    . . .
    "lint": "eslint . --fix"
  }
}
```

Then run ESLint:

```bash
$ npm run lint
```

To prevent ESLint from linting certain files or directories, create a `.eslintignore` file in your project root, and place the patterns for files to ignore therein. Here's an example in which all generated files in the `dist` folder are ignored:

```plaintext
dist
```

You can also decide to omit every single JavaScript file in your project directory with the following pattern:

```plaintext
**/*.js
```

Note that everything in the `node_modules` folder, and files or folders that begin with a dot character (except eslint config files), are ignored automatically, so there's no need to place patterns matching such files in your `.eslintignore` file.

## Debug Your TypeScript Code with Visual Studio Code

Debugging TypeScript source files is easy and straightforward with the help of `ts-node` and Visual Studio Code. All you need to do is create a launch configuration file (`launch.json`) within the `.vscode` directory in the project root and populate the file with the following code:

```json
{
  "version": "0.1.0",
  "configurations": [
    {
      "name": "Debug main.ts",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceRoot}",
      "runtimeArgs": ["-r", "ts-node/register"],
      "args": ["${workspaceRoot}/src/main.ts"]
    }
  ]
}
```

The `ts-node/register` method is preloaded in the above file to handle TypeScript source files correctly. Secondly, the name of the TypeScript file to run when starting a debugging session is provided as the first value in the `args` property.

Go ahead and start debugging your Node.js project by pressing F5 on your keyboard. Try to set a breakpoint, then inspect the values in the current scope once the breakpoint is hit. It should work as expected, without issues!

![Debugging TypeScript in VSCode](https://blog.appsignal.com/images/blog/2022-01/debugging-typescript-vscode.png)

## Deploying TypeScript to Production

According to its author, [ts-node is safe to use in production](https://github.com/TypeStrong/ts-node/issues/104#issuecomment-250252708). Nonetheless, to reduce the start-up time of the server and prevent additional memory usage from keeping the compiler in memory, it's better to compile the source files beforehand. Execute the resulting JavaScript code with the `node` command when deploying to production.

## Node.js and TypeScript: Summing Up

In this article, you learned how to configure a Node.js project with TypeScript support and run TypeScript source files directly, without an intermediate compilation step.

We also covered how type definition files work, and how to take advantage of predefined type definition files for popular npm packages so that the TypeScript compiler fully validates all third-party dependencies.

Finally, we discussed how to debug TypeScript files in VSCode, and what to do when deploying your TypeScript project to production.

Thanks for reading, and happy coding!

**P.S. If you liked this post, [subscribe to our JavaScript Sorcery list](https://blog.appsignal.com/javascript-sorcery) for a monthly deep dive into more magical JavaScript tips and tricks.**

**P.P.S. If you need an APM for your Node.js app, go and [check out the AppSignal APM for Node.js](https://www.appsignal.com/nodejs).**