Integrate Type Checking with Node.js using TypeScript: A Tutorial
With Node.js and Express, you can easily create a JavaScript server. Then the obvious question would be, “Why Typescript?”
While JavaScript remains the most popular scripting language, TypeScript emerges as a great alternative when your application becomes more complex or when you collaborate with a distributed team of developers.
While both languages share the same ECMA Script foundation, TypeScript adds static typing and other features to improve the development experience, catch potential errors early, and enhance code maintainability.
Typescript vs. JavaScript: Should you make the Switch?
In this article, we will learn how to integrate Typescript into a Node.js Project.
- Prerequisites
- Step 1 - Install Typescript
- Step 2 - Initialize a Typescript Configuration
- Step 3 - Configure tsconfig.json
- Step 4 - Install Typescript Node
- Step 5 - Update Typescript Code
- Step 6 - Update NPM Scripts
- Step 7 - Install @types for External Modules
- Step 8 - Run Type Checking
- Step 9 - Fixing Type Errors
Prerequisites
Before we begin with the tutorial, you need to tick these prerequisites check box. Make sure you have all of them updated and running.
- Latest version Node.js
- NPM
- Basic Typescript Understanding
Demo Node.js Project
To get started, let's create a simple Node.js project for a to-do list API, and then we'll integrate TypeScript into it. Start by creating a new directory for your project and navigate into it:
mkdir todo-api
cd todo-api
Initialize a new Node.js project using -y
. This -y
flag utilizes the default settings captured by npm, bypassing repeated questions asking for project details. After that, install the necessary dependencies and create a simple to-do API list under file name index.js
.
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
app.use(bodyParser.json());
let todos = [];
app.get('/todos', (req, res) => {
res.json(todos);
});
app.post('/todos', (req, res) => {
const { text } = req.body;
const newTodo = { id: todos.length + 1, text };
todos.push(newTodo);
res.status(201).json(newTodo);
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
After this, launch this development server on port 3000 with the following command:
npm run dev
Step 1 - Install Typescript
Now that we have installed the demo Node.js project, you can go ahead and install the Typescript compiler.
npm install typescript
By installing TypeScript locally, the version is recorded in your project's package.json
file for future clones, ensuring that any changes between versions won't break your project.
Once you have installed typescript, tsc
command will be available in your project which you can access through npx
npx tsc --version
This will output the current version of the typescript installed on your system. Usually, it keeps changing once in every three months.
Step 2 - Initialize a Typescript Configuration
Tsconfig.json
is the default typescript configuration file. A configuration file manages various project settings as default options and makes it easier to modify our compiler settings later.
Hence, before you start to compile javascript source files, it is advised that you set up a typescript configuration file - tsconfig.json
. Because, if you start adding files before having a config file, typescript might start to throw errors.
So create a tsconfig.json
file in the root of your project. You can generate a basic configuration file using the following command:
tsc --init
Tsconfig.json
hosts a variety of compiler options. I have listed out a few important ones here:
- Target - Specifies the ECMAScript target version for the generated JavaScript code. Common values include "es5", "es6", "es2015", "es2016", etc.
- Module - Specifies the module system for the generated JavaScript. Common values include "commonjs", "amd", "es6", etc.
- Strict - Enables strict type-checking options. This is a combination of several strict options like
noImplicitAny
,strictNullChecks
, etc.. esModuleInterop
- Enables interoperability between CommonJS and ES Module (ESM) styles.skipLibCheck
- Skips type-checking of declaration files (.d.ts files) to improve compilation speed.forceConsistentCasingInFileNames
- Ensures that file names in TypeScript code match the case of the actual files on disk. Helps prevent issues on case-sensitive file systems.outDir
- Specifies the directory where the compiled JavaScript files should be placed. Useful for keeping the source and output files in separate directories.rootDir
- Specifies the root directory of input files. TypeScript uses this to determine the file structure and preserve it in the output directory.include and exclude
- Specifies which files to include or exclude from the compilation. Use glob patterns to define the file matching criteria.
Step 3 - Configure tsconfig.json
Edit the tsconfig.json
file:
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
This configuration sets up TypeScript with strict type checking and includes Node.js types.
Step 4 - Update Typescript Code
To enable TypeScript to work seamlessly with Node.js, you can install ts-node
:
npm install --save-dev ts-node
Create a new src
directory and move your index.js
file into it. Rename it to index.ts.
Update index.ts with these TypeScript features:
import express from "express";
import bodyParser from "body-parser";
const app = express();
const port = 3000;
app.use(bodyParser.json());
interface Todo {
id: number;
text: string;
}
let todos: Todo[] = [];
app.get("/todos", (req, res) => {
res.json(todos);
});
app.post("/todos", (req, res) => {
const { text } = req.body as { text: string };
const newTodo: Todo = { id: todos.length + 1, text };
todos.push(newTodo);
res.status(201).json(newTodo);
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
Now, if you attempt to execute the index.ts file using Node, similar to what we did with our index.js
file, you will encounter an error. This is because Node doesn’t inherently support the direct execution of TypeScript files. The next section discusses running TypeScript files in the terminal using a Node package.
Step 5 - Install Typescript Node
Node doesn't support .ts
files by default. But we can use ts-node to overcome this limitation.
npx ts-node src/index.ts
The main advantage of using ts-node is that it removes the extra step of code transpilation and allows you to work with TypeScript code directly in a Node.js environment. Additionally, it is useful when working with TypeScript files in the Node terminal.
Step 6 - Update NPM Scripts
When you install ts-node as a development dependency, after which you can update your package.json file with TypeScript-related scripts as follows:
{
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
}
}
Step 7 - Install @types for External Modules
If you're using external modules without TypeScript support, you may need to install type definitions for those modules. For example:
npm install --save-dev @types/express
Replace express with the module name you are using.
Step 8 - Run Type Checking
Now, you can run your TypeScript code using:
npm start
nodemon is a utility that helps in automatically restarting the Node.js application when file changes are detected. While nodemon itself is not designed for type checking, you can use it in conjunction with other tools, such as tsc (TypeScript Compiler) or ts-node
, to achieve a development workflow that includes both automatic restarting and type checking.
npm install --save-dev nodemon ts-node
Add a script in your package.json that uses nodemon with ts-node:
{
"scripts": {
"start:dev": "nodemon --exec ts-node app.ts",
"build": "tsc",
"start": "node dist/app.js"
}
}
- "start:dev": This script uses nodemon to monitor file changes and restart the application when changes are detected. It uses ts-node to run the TypeScript code directly.
- "build": This script compiles TypeScript code to JavaScript using
tsc
. - "start": This script runs the compiled JavaScript code.
Now, you can start your development server with automatic restarts and type checking using:
npm run start:dev
This setup allows you to benefit from automatic restarts provided by nodemon while developing TypeScript applications and ensures that your TypeScript code is type-checked during development.
Step 9 - Fixing Type Errors
1. Review TypeScript Configuration:
Make sure your tsconfig.json
file is correctly configured. Pay attention to the compilerOptions
section, especially settings related to strict typing (strict, noImplicitAny, etc.). Adjust these settings according to your project's requirements.
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"strict": true
// ... other options
}
// ... other configurations
}
2. External Libraries and Type Definitions:
If your project uses external libraries, ensure that you have installed the corresponding type definitions (@types packages) or that the libraries themselves include TypeScript support. Install missing type definitions using npm:
npm install --save-dev @types/library-name
3. Type Annotations in Functions and Variables:
Explicitly annotate the types of function parameters, return values, and variables. TypeScript relies on these annotations for type checking.
function greet(name: string): string {
return `Hello, ${name}!`;
}
4. Handle Potential Null/Undefined Values:
If TypeScript complains about potential null or undefined values, consider checking or asserting before using the variable.
let data: SomeType | null = fetchData();
if (data !== null) {
// Now TypeScript knows that 'data' is not null
process(data);
}
5. Update External Libraries:
If you're using third-party libraries, ensure that they are up-to-date. Sometimes, type issues can be resolved by updating to a newer version of the library that includes better TypeScript support.
Pay close attention to the error messages provided by TypeScript. They often include detailed information about the specific type error and can guide you in fixing the issue.
By addressing these common aspects, you should be able to resolve many type errors in a Node.js project using TypeScript. Remember that TypeScript is designed to catch potential issues early in the development process, leading to more robust and maintainable code.
Conclusion
A Node.js application's transition to TypeScript may first appear overwhelming, but with the correct resources and techniques, it can be an exciting and easy process.
Because of TypeScript's strong type system, which can identify any problems at compile time, your codebase will be easier to maintain and less likely to include mistakes at runtime.
You can ensure that your Node.js project is configured correctly to make use of all the advantages that TypeScript has to offer by following the procedures outlined in this article.
You will finally notice that your development process is more effective and less prone to errors with the additional type safety and better tooling.
Monitor Your Node.js Applications with Atatus
Atatus keeps track of your Node.js application to give you a complete picture of your clients' end-user experience. You can determine the source of delayed response times, database queries, and other issues by identifying backend performance bottlenecks for each API request.
Node.js performance monitoring made bug fixing easier, every Node.js error is captured with a full stack trace and the specific line of source code marked. To assist you in resolving the Node.js error, look at the user activities, console logs, and all Node.js requests that occurred at the moment. Error and exception alerts can be sent by email, Slack, PagerDuty, or webhooks.