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:
$ npm install typescript --save-dev
Once the above command succeeds, you can check the current version through the following command:
$ 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.
$ npm install typescript --global
$ tsc --version
Version 4.4.3
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.
$ touch tsconfig.json
Here's a basic configuration that you can start with:
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {},
"include": ["src"],
"exclude": ["node_modules"]
}
The above configuration file extends the base configuration provided by the TypeScript team for Node.js v16. 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 v16 to your project's devDependencies
through the command below. Base tsconfig.json
packages also exist for Node 10, Node 12, and Node 14 at the time of writing.
$ 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:
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:
$ 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 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 so that the TypeScript compiler can understand and validate all the built-in Node.js APIs.
Here's how:
$ 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:
"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:
$ 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:
{
"compilerOptions": {
"outDir": "dist"
}
}
Subsequent compilations will emit the JavaScript output into a dist
folder.
.
├── 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:
$ npm install ts-node --save-dev
Afterwards, execute the main.ts
file with the ts-node
command:
$ 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:
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:
$ 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 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:
$ 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:
$ 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. 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.
$ 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:
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:
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:
$ npx eslint . --fix
You can also create a lint
script in your package.json
file as follows:
{
"scripts": {
. . .
"lint": "eslint . --fix"
}
}
Then run ESLint:
$ 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:
dist
You can also decide to omit every single JavaScript file in your project directory with the following pattern:
**/*.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:
{
"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!
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.
Top comments (0)