Using TypeScript in Node.js

A Complete Guide to Using TypeScript in Node.js

Better Stack Team
Updated on May 25, 2023

TypeScript is a superset of JavaScript that brings static typing capabilities to the language. Since its introduction in 2011, it has steadily gained adoption, and is now the preferred way for many developers and organizations to write code for the browser or for server runtime environments like Node.js or Deno.

Unlike Deno, Node.js does not support TypeScript natively, so additional work is required to bring type checking to the runtime so that Node.js projects can also benefit from the increased safety that utilizing TypeScript provides.

By following through with this tutorial, you will become familiar with the following aspects of utilizing TypeScript to develop Node.js applications:

  • Installing and configuring the TypeScript compiler.
  • Strategies for migrating an existing Node.js codebase to TypeScript.
  • Integrating TypeScript with the NPM ecosystem.
  • Executing TypeScript source files directly without compilation.
  • Fixing errors caused by missing types.
  • Setting up linting and formatting for TypeScript files.
  • Debugging TypeScript in Chrome or VS Code.
  • Deploying your TypeScript application to production.


Before you proceeding with this tutorial, ensure that you have a recent version of Node.js and npm installed. We tested the setup described in this tutorial with v16.14.2 and v8.5.0, respectively.

We also assume some familiarity with TypeScript syntax and benefits. Basically, we expect that you're already sold on migrating to TypeScript or using it for your next Node.js project. Therefore, we won't attempt to convince you on the benefits of using TypeScript in this tutorial, but will only focus on getting it working seamlessly in Node.js' contexts.

Better Uptime – Monitors (Dark Mode).png

🔭 Want to get alerted when your Node.js app stops working?

Head over to Better Uptime and start monitoring your endpoints in 2 minutes

Step 1 — Downloading the demo project

To demonstrate each step involved in getting TypeScript working seamlessly in a Node.js project, we will utilize a demo Node.js application that reports the current price of Bitcoin in various currencies (both crypto and fiat). Of course, you can also practice the concepts discussed in this article by executing the steps below in some other project if you wish.

Start by running the command below to clone the repository to your machine:

git clone

Change into the newly created btc-exchange-rates directory and run the command below to download all the project's dependencies:

npm install

Afterward, launch the development server on port 3000 by executing the command below:

npm run dev

You should observe the following output:

> btc-exchange-rates@1.0.0 dev
> nodemon server.js

[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server.js`
server started on port: 3000
Exchange rates cache updated

Visit http://localhost:3000 in your browser to see the application in action:

BTC Exchange Rates screenshot

We will install and set up the TypeScript compiler in our Node.js application in the next step.

Step 2 — Installing and configuring TypeScript

Now that we have our demo application cloned and working locally let's go ahead and install the TypeScript compiler in our project through the command below. It's better to install TypeScript locally to register the installed version in your package.json file to ensures that everyone who clones your project in the future gets the same version of TypeScript. This is an important precaution as there are often breaking changes between versions.

npm install typescript

Once installed, you will have the tsc command available in your project, which you can access through npx as shown below:

npx tsc --version
Version 4.6.3

You may see a different version of TypeScript depending on when you're following this tutorial. In general, you can expect a new release around every three months.

We need to set up a configuration file (tsconfig.json) for our project before we can start compiling our source files. If you attempt to run the TypeScript compiler without setting up a config file, you will get an error code. You can also specify command-line flags instead, but a config file is more convenient.

Go ahead and create the tsconfig.json file in the root of your project directory:

code tsconfig.json

Once the file is open in your text editor, paste in the following contents:

  "extends": "@tsconfig/node16/tsconfig.json",
  "include": ["src/**/*"],
  "exclude": ["node_modules"]

TypeScript provides a host of configuration options to help you specify what files should be included and how strict you want the compiler to be. Here's an explanation of the basic configuration above:

  • extends: provides a way to inherit from another configuration file. We are utilizing the base config for Node v16 in this example, but feel free to utilize a more appropriate base configuration for your Node.js version.
  • include: specifies what files should be included in the program.
  • exclude: specifies the files or directories that should be omitted during compilation.

Another critical property not shown here is compilerOptions. It's where the majority of TypeScript's configuration takes place, and it covers how the language should work. When is omitted as above, it defaults to the compilerOptions specified in the base configuration or the TypeScript compiler defaults.

The base configuration referenced above is provided as an NPM package, so you need to install it:

npm install --save-dev @tsconfig/node16

After installing the base config, go ahead and run the TypeScript compiler in your project root:

npx tsc

You should observe the following error indicating that TypeScript did not find anything to compile due to the lack of a .ts file in the src directory.

error TS18003: No inputs were found in config file '/home/ayo/dev/demo/btc/tsconfig.json'. Specified 'include' paths were '["src/**/*"]' and 'exclude' paths were '["node_modules"]'.

Found 1 error.

You can fix this error in two ways. Either you add a .ts file in your src directory, or you specify the allowJs compiler option so that the TypeScript compiler also recognizes JavaScript files. We will use the latter option as it can be used to convert a JavaScript project to TypeScript incrementally. We also need to specify the outDir option, which specifies a path relative to the tsconfig.json file where the compilation output will be placed.

  "extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"outDir": "dist"
"include": ["src/**/*"], "exclude": ["node_modules"] }

After saving the file, run the TypeScript compiler once more. You will observe that no output is produced, which means the compilation was successful.

npx tsc

A dist directory will be in your project root with a server.js file which is the output emitted after the compilation of the src/server.js file.

You can update your dev script to run this file instead of the source server.js file:

"scripts": {
"dev": "nodemon dist/server.js"

Step 3 — Type checking JavaScript files

We are currently set up to compile both JavaScript and TypeScript files, but type checking will not be performed on JavaScript files unless we specify the checkJs compiler option. If you're just migrating to TypeScript, you may want to turn this option on to get some benefits of type checking without switching your entire codebase from .js to .ts files.

  "extends": "@tsconfig/node16/tsconfig.json",
  "compilerOptions": {
    "allowJs": true,
"checkJs": true,
"outDir": "dist" }, "include": ["src/**/*"], "exclude": ["node_modules"] }

The problem with enabling this option is that, in a medium to large project, you are likely to get a huge amount of errors, and it really doesn't make much sense to spend a lot of time and effort fixing type errors without migrating to .ts files from the get-go.

If you do decide to type check your JavaScript files, you can do so one at a time by utilizing the // @ts-check comment at the top of each file. This way, you won't need to set checkJs to true in your tsconfig.json just to type check some specific files without getting errors from other ones.

// @ts-check
const express = require('express');
const path = require('path');
. . .

You can also opt-out of type checking for a single file by using the // @ts-nocheck. This is useful if checkJs is enabled, but you want to suppress errors from a problematic file that you don't have time to fix right away. This option makes more sense if you have a small codebase that you want type checked immediately but you want to retain the ability to opt out in specific files.

// @ts-nocheck
const express = require('express');
const path = require('path');
. . .

A third option which may be used with // @ts-check or checkJs enabled is // @ts-ignore. It lets you opt out of type checking on a line-by-line basis. You can also use // @ts-expect-error to indicate that you expect an error on a line (only use if an error is present or the compiler will report that the comment wasn't necessary). Learn more about when to use ts-ignore or ts-expect-error here.

// @ts-expect-error
const app = express();

// or

// @ts-ignore
const app = express();

Note that all the special comments mentioned above work in both JavaScript files (if allowJs is true) and TypeScript files. Since our demo application has a single file in it, we won't be utilizing checkJs or any one of the other approaches described in this section. Instead, we will migrate to it to TypeScript and start fixing any type errors that ensue.

Step 4 — Migrating your JavaScript files to TypeScript

Migrating from JavaScript to TypeScript involves changing the extension from .js to .ts. This works because every valid JavaScript program is also a TypeScript program so that's all you need to start writing TypeScript code.

mv src/server.js src/server.ts

In a Node.js project, you'll also need to install the @types/node package to provide type definitions for Node.js APIs which are subsequently auto-detected by the compiler.

npm install --save-dev @types/node

Afterward, change your imports from require() to the ES6 import syntax. Since the module option is set to commonjs in the Node 16 base configuration, the compiler will generate CommonJS compatible code which can be executed directly by the Node.js runtime.

import express from 'express';
import path from 'path';
import axios from 'axios';
import morgan from 'morgan';
import NodeCache from 'node-cache';
import { format } from 'date-fns';
const appCache = new NodeCache(); const app = express();

At this point, you can run the TypeScript compiler to see if we get any errors:

npx tsc
src/server.ts:1:21 - error TS7016: Could not find a declaration file for module 'express'. '/home/ayo/dev/demo/btc/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/server.ts:4:20 - error TS7016: Could not find a declaration file for module 'morgan'. '/home/ayo/dev/demo/btc/node_modules/morgan/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/morgan` if it exists or add a new declaration (.d.ts) file containing `declare module 'morgan';`

4 import morgan from 'morgan';

src/server.ts:16:15 - error TS7006: Parameter 'message' implicitly has an 'any' type.

16       write: (message) => console.log(message.trim()),

src/server.ts:64:21 - error TS7006: Parameter 'req' implicitly has an 'any' type.

64 app.get('/', async (req, res, next) => {

src/server.ts:64:26 - error TS7006: Parameter 'res' implicitly has an 'any' type.

64 app.get('/', async (req, res, next) => {

src/server.ts:64:31 - error TS7006: Parameter 'next' implicitly has an 'any' type.

64 app.get('/', async (req, res, next) => {

src/server.ts:74:35 - error TS2571: Object is of type 'unknown'.

74       lastUpdated: dateFns.format(data.timestamp, 'LLL dd, yyyy hh:mm:ss a O'),

Found 7 errors in the same file, starting at: src/server.ts:1

Most of the errors above indicate that the compiler could not figure out the types for the referenced entities underlined with tilde characters (~), so it implicitly assigns type any to them. This produces an error because implicitly assigning the any type is disallowed in the base configuration (via "strict": true). The reason why TypeScript cannot figure out the types for the entities exported by these libraries is that they are written in JavaScript so there is no type information available (unless the library provides one by default, which most don't). In the next section, we will discover some strategies for solving this problem.

Step 5 — Fixing type errors caused by third-party libraries

The TypeScript compiler is able to automatically detect the types of any library that's written in TypeScript (like date-fns), so that it can guarantee that you're using the correct types and notify you if there's mismatch at compile-time. However, most NPM packages are written in JavaScript, so no type information is available, which leads to the compiler implicitly assigning the any type to the entire library.

This is problematic because you don't get type safety with the any type, so even when you supply an incorrect type or do something illegal (such as calling a non-existent method), the compiler will not be able to detect that for you which may lead to runtime problems in production negating the benefit of using TypeScript in the first place.

The noImplicitAny compiler option was provided to address this problem. When it's set to true, the compiler will throw an error when it cannot infer the type for an entity instead of assigning any to it. This option is part of the strict family so it is enabled when strict is true.

JavaScript library authors can ensure that their packages are type-checked when utilized in a TypeScript project by providing type declaration files (.d.ts) that describe the shape of the library to the compiler so that it can help prevent misuse. This also improves the development experience in compatible editors because you get a much nicer auto-completion.

Some popular JavaScript libraries have adopted the practice of including type declarations in the main package so that they also work seamlessly in TypeScript codebases without compromising the type safety of the project. An example is axios whose types are included in the main repository so that it is downloaded alongside its NPM package and automatically detected by the TypeScript compiler.

For the libraries that don't provide declaration files, additional user intervention is required to ensure that type safety is retained when utilizing those libraries. This often comes in the form of installing community-sourced type declarations for the library (published under the @types scope on NPM) or creating a declaration file from scratch. We used the same type technique earlier to get type checking for standard Node.js APIs (by installing the @types/node package) since those APIs are written in JavaScript.

If you look at the error messages from the previous section, you'll see this exact situation play out. express and morgan are two JavaScript libraries that do not provide declaration files, and this is reflected in the first two error messages. The fix, as suggested in messages, is to install the type definitions for each affected library if they are in the DefinitelyTyped repository. Packages that are widely used are likely to have community-sourced type declaration files in this repository.

Give it a go by installing the type definitions for express and morgan through the command below:

npm install --save-dev @types/express @types/morgan

After installing the packages, run the TypeScript compiler once again. All the "implicit any" errors should be gone now as the compiler can automatically recognize the newly installed types and check your code against them.

npx tsc
src/server.ts:74:35 - error TS2571: Object is of type 'unknown'.

74       lastUpdated: dateFns.format(data.timestamp, 'LLL dd, yyyy hh:mm:ss a O'),

Found 1 error in src/server.ts:74

If you try something illegal now, the compiler will bring your attention to the problem straight away:

// attempting to use a method that does not exist
npx tsc
src/server.ts:21:5 - error TS2339: Property 'misuse' does not exist on type 'Express'.

21 app.misuse(morganMiddleware);

Let's briefly discuss what to do if you can't find type declarations for the library you're using under the @types scope on NPM, although this is unlikely to happen if you stick to widely used packages. If you want to retain type safety while using some obscure library or internal package, then you'll need to create type declaration files for the package. Doing this is outside the scope of this tutorial, but the information contained in the TypeScript handbook should help guide you through the process.

🔭 Want to centralize and monitor your Node.js application logs?

Head over to Logtail and start ingesting your logs in 5 minutes.

Step 6 – Fixing other type errors

We've successfully solved the "implicit any" errors caused by a lack of type information in JavaScript libraries, but we still have one more error to address:

src/server.ts:74:35 - error TS2571: Object is of type 'unknown'.

74       lastUpdated: dateFns.format(data.timestamp, 'LLL dd, yyyy hh:mm:ss a O'),
Found 1 error in src/server.ts:74

This error indicates that TypeScript was unable to detect the type of the data entity so it assigns unknown to it, which is a type that only assignable to any and unknown itself. This data entity is an object containing the exchange rates object received from the Coin Gecko API, and a date object representing the timestamp at which the data was last updated.

To fix this error, we need to inform TypeScript of the shape of this entity by creating custom types. Go ahead and add the highlighted lines below to your src/server.ts file:

. . .

app.set('views', path.join(__dirname, '..', 'views'));

type Currency = {
name: string;
unit: string;
value: number;
type: string;
type Rates = {
rates: Record<string, Currency>;
type ExchangeRateResult = {
timestamp: Date;
exchangeRates: Rates;
. . .

The ExchangeRateResult type describes the shape of the data entity. It is an object that contains a timestamp property and an exchangeRates object. The exchangeRates object has a single property (rates) that contains many additional objects with the following shape:

"btc": {
  "name": "Bitcoin",
  "unit": "BTC",
  "value": 1,
  "type": "crypto"

The above object can be described in TypeScript using the Record<Keys, Type> type. It is used to construct an object type whose property keys are Keys and whose property values are Type. In this case, we've set the keys type as string and the value type as Currency. We can probably increase type safety by specifying the key as a union of all the object key names received in the API response, but using a generic string will suffice for this tutorial.

At this point, we must annotate the return types of the getExchangeRates() and refreshExchangeRates() functions as shown below:

async function getExchangeRates(): Promise<Rates> {
const response = await axios.get( '', { headers: { Accept: 'application/json', }, } ); return; }
async function refreshExchangeRates(): Promise<ExchangeRateResult> {
const rates = await getExchangeRates(); const result = { timestamp: new Date(), exchangeRates: rates, }; appCache.set('exchangeRates', result, 600); console.log('Exchange rates cache updated'); return result; }

Finally, in the root route, we must specify the type of the data object as shown below to reflect that it can be an ExchangeRateResult or undefined if not found in the cache.

. . .
let data: ExchangeRateResult | undefined = appCache.get('exchangeRates');

if (data === undefined) {
  data = await refreshExchangeRates();
. . .

At this point, TypeScript has all the information it needs to check our use of the data type and ensure that we conform to the defined contact, so the "unknown type" error should be gone now.

npx tsc

Step 7 — Run TypeScript source files with ts-node

Now that we've scaled our initial migration hurdles, we can now turn our attention to quality of life improvements that will help with making your TypeScript project feel like a first-class citizen in the Node.js ecosystem.

We're currently using the tsc command to compile our TypeScript source files into JavaScript code, and nodemon to watch and restart the server when a change is detected. Running the tsc command repeatedly in development can get tedious quickly, so you should probably use the --watch flag to compile the source files automatically after editing:

npx tsc --watch
[8:47:09 AM] Starting compilation in watch mode...

[8:47:13 AM] Found 0 errors. Watching for file changes.

Despite this change, we still have to run two commands initially to get our development environment up and running. We can reduce this to just one by utilizing the ts-node CLI to execute .ts files directly. Go ahead and install it in your project through the command below:

npm install ts-node --save-dev

Afterward, quit the nodemon and tsc processes by pressing Ctrl-C, then execute thesrc/server.ts file directly with ts-node:

npx ts-node src/server.js

You should observe the following output:

server started on port: 3000
Exchange rates cache updated

Under the hood, the ts-node command transpiles the TypeScript source files with tsc and executes the JavaScript output with node. Utilizing ts-node in this manner will add some overhead to the startup time of your application, but you can make it faster by opting out of type checking through the --transpile-only or -T flag if type validation isn't essential at a given moment.

ts-node does not provide a watch mode that automatically restarts the server when a change is detected, but we can easily integrate it with nodemon and get the best of both worlds. Go ahead and create a nodemon.json file in your project root and update its contents as follows:

  "watch": ["src"],
  "ext": ".ts",
  "ignore": [],
  "exec": "ts-node --files ./src/server.ts"

Afterward, change the dev script in your package.json to nodemon:

"scripts": {
"dev": "nodemon"

At this point, you can run a single command to compile transpile TypeScript to JavaScript and auto reload your server when changes are made to the source files.

npm run dev
> btc-exchange-rates@1.0.0 dev
> nodemon

[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/*
[nodemon] watching extensions: ts
[nodemon] starting `ts-node --files ./src/server.ts`
server started on port: 3000
Exchange rates cache updated

Step 8 — Linting TypeScript with ESLint

Let's turn our attention to improving your TypeScript code quality by defining coding conventions and automatically enforcing them, starting with linting through ESLint. TSLint used to be another option for linting TypeScript code, but it is now deprecated so you should not use it anymore.

Go ahead and install eslint in your project through the command below:

npm install eslint --save-dev

ESLint was originally made to lint only JavaScript code, so you have to install some additional plugins to get it working with TypeScript:

npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin

Afterward, create a .eslintrc.js file in your project root and update its contents as follows:

  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2021,
    "sourceType": "module",
    "project": "tsconfig.json"
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  "env": {
    "es6": true,
    "node": true
  "rules": {},
  "ignorePatterns": ["dist"]

The @typescript-eslint/parser package parses TypeScript source files into a format that is understandable by the eslint program, while the @typescript-eslint/eslint-plugin package provides some recommended linting rules for TypeScript code.

At this stage, you should get linting messages in your editor provided that you have the relevant ESLint plugin installed. You can also create a lint script that can be executed from the command line:

"scripts": {
  "dev": "nodemon",
"lint": "eslint . --fix"
npm run lint
> btc-exchange-rates@1.0.0 lint
> eslint . --fix

   80:31  warning  'next' is defined but never used             @typescript-eslint/no-unused-vars
  101:7   warning  'server' is assigned a value but never used  @typescript-eslint/no-unused-vars

✖ 2 problems (0 errors, 2 warnings)

You can utilize the rules object in the ESLint configuration to override any linting rules. For example, if you don't want unused variables to be reported as issues as shown above above, you can disable it through the following code:

"rules": {
  "@typescript-eslint/no-unused-vars": "off"

Do check out the ESLint docs and typescript-eslint repository to learn more about configuring ESLint for JavaScript and TypeScript.

Step 9 — Formatting TypeScript code with Prettier

In this section, we will configure Prettier for auto formatting TypeScript code and make it play well with ESLint's rules. Start by installing the prettier package as shown below:

npm install --save-dev prettier

Afterward, create a Prettier configuration and update its contents as shown below:

  "printWidth": 80,
  "singleQuote": true,
  "trailingComma": "es5",
  "bracketSpacing": true

Prettier supports TypeScript out of the box, so there's no need to install any plugins to get it working. You can run the command below to format your files, and it should just work:

npx prettier --write src/server.ts
src/server.ts 621ms

To ensure that Prettier's formatting rules do not conflict with ESLint, install the following two packages in your project:

npm install --save-dev eslint-config-prettier eslint-plugin-prettier

Afterward, update your .eslintrc.json file as shown below:

"extends": [

The highlighted line above enables the eslint-config-prettier and eslint-plugin-prettier plugins. The former disables the ESLint rules that conflict with Prettier, while the latter reports Prettier errors as ESLint issues. It also ensures that Prettier errors are fixed when the --fix option is used in ESLint. For an optimal development experience, you should configure your editor so that it can run ESLint's --fix command when a file is saved so you don't have to run a command to fix your formatting.

Step 10 — Debugging TypeScript code with Visual Studio Code or Chrome DevTools

Visual Studio Code supports TypeScript debugging through its built-in Node.js debugger. If you have it installed on your computer, you can create a launch configuration file (.vscode/launch.json) in your project root to specify VS Code's debugging behavior for your application.

mkdir .vscode
code .vscode/launch.json

Place the following code into the file:

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

There are two things worth noting here. The runtimeArgs value is passed to node to register the ts-node CLI for handling TypeScript files. Secondly, the file to run when launching a debugging session is given as the first argument in the args property. You can view the relevant VS Code docs to learn more about configuring your application's debugging configuration setup.

Assuming VS Code is open, you can start your debugging session by pressing F5. Ensure that no other instances of your server is currently running or you might get an EADDRINUSE error. Afterward, you can set breakpoints and inspect values in the path of execution as usual.

Debugging TypeScript in VS Code

If you want to utilize the Chrome debugger instead of VS code, run the command below in your project root:

node -r ts-node/register --inspect src/server.ts

You should see the following output:

Debugger listening on ws://
For help, see:
server started on port: 3000
Exchange rates cache updated

Afterward, launch Chrome and open the developer tools by pressing F12. Once open, click the Node.js icon on the top left to open the dedicated DevTools for Node.js.

Node.js icon in Chrome DevTools

You should be able to view the Node.js console in the Console tab, and you can go to the Sources tab to debug your code as usual.

Debugging Node.js in Chrome DevTools

Step 11 — Deploying to production

ts-node should be safe to use in production, but we recommend running the compiled JavaScript output instead to eliminate the overhead of keeping the TypeScript compiler in memory. If you'd like to learn more about how to deploy, manage and scale Node.js applications in production, see our linked article on the subject.


In this article, you've learned how to migrate a Node.js application to TypeScript, and how to set it up like a first-class citizen in the Node.js ecosystem. We started by discussing how to install and configure the TypeScript compiler, then we explored a few strategies for migrating your existing JavaScript project to TypeScript. Afterward, we diagnosed a few different types of TypeScript errors and how to fix them, then we detailed the steps involved in linting, formatting, and debugging TypeScript code.

You can find the entire source code used for this tutorial in the prod branch of this GitHub repository. Thanks for reading, and happy coding!

Check Uptime, Ping, Ports, SSL and more.
Get Slack, SMS and phone incident alerts.
Easy on-call duty scheduling.
Create free status page on your domain.
Got an article suggestion? Let us know
Next article
How to Deploy Node.js Applications with Docker
Learn how to deploy your Node.js application in a Docker container and some best practices for writing Docker files
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.