javascript

How to Set Up a Node.js Project with TypeScript

Ayooluwa Isaiah

Ayooluwa Isaiah on

Last updated:

How to Set Up a Node.js Project with TypeScript

This post was updated on 8 August 2023 to use the latest LTS version of Node and TypeScript 5.

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 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 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 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, 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, and it can be downloaded into your project through a package manager like npm or yarn:

Shell
$ npm install typescript --save-dev

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

Shell
$ npx tsc --version Version 5.1.6

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.

Shell
$ npm install typescript --global $ tsc --version Version 5.1.6

If you use a Node.js environment manager like Volta, 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 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.

Shell
$ touch tsconfig.json

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

JSON
{ "extends": "@tsconfig/node-lts/tsconfig.json", "compilerOptions": {}, "include": ["src"], "exclude": ["node_modules"] }

The above configuration file extends the base configuration provided by the TypeScript team for the latest LTS version of Node.js (currently v18). Additional options 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.

Before you proceed, ensure you add the base configuration package for Node.js LTS to your project's devDependencies through the command below. Base tsconfig.json packages also exist for Node 10, Node 12, Node 14, up to Node 20, at the time of writing.

Shell
$ npm install @tsconfig/node-lts --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:

Shell
$ npx tsc

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

Shell
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'. 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 in the same file, starting at: src/main.ts:3

This error occurs because the lib compiler option set in the base configuration for Node.js LTS 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 so that the TypeScript compiler can understand and validate all the built-in Node.js APIs. Here's how:

Shell
$ 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"; Object.defineProperty(exports, "__esModule", { value: true }); 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:

Shell
$ 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 compiler option in your tsconfig.json file:

JSON
{ "compilerOptions": { "outDir": "dist" } }

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

Shell
. ├── 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 CLI to execute .ts files directly. Go ahead and install the ts-node package using the command below:

Shell
$ npm install ts-node --save-dev

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

Shell
$ 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.

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

Shell
$ npx ts-node src/main.ts TSError: Unable to compile TypeScript: src/main.ts:1:21 - error TS7016: Could not find a declaration file for module 'express'. '/path/to/project/node_modules/express/index.js' implicitly has an 'any' type. Try `npm i --save-dev @types/express` if it exists or add a new declaration (.d.ts) file containing `declare module 'express';` 1 import express from "express"; ~~~~~~~~~ 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.

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 file, the 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.

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:

Shell
$ 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 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 in your project with the command below:

Shell
$ 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 for ESLint. Note that with the release of ESLint v9.0.0 the config system will change and the config file will be named eslint.config.js.

To add TypeScript support to ESLint, install the @typescript-eslint/parser and @typescript-eslint/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.

Shell
$ 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 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:

Shell
$ 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:

Shell
$ 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

Deploying TypeScript to Production

According to its author, ts-node is safe to use in production. 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 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.

Ayooluwa Isaiah

Ayooluwa Isaiah

Ayo is a Software Developer by trade. He enjoys writing about diverse technologies in web development, mainly in Go and JavaScript/TypeScript.

All articles by Ayooluwa Isaiah

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps