Page Content

Tutorials

How to use Node.js in TypeScript?

Node.js in TypeScript

By introducing static types to JavaScript, TypeScript (TS) improves code quality, maintainability, and scalability for application development. This integration offers substantial benefits for Node.js backend environments. Intentionally a typed superset of JavaScript, TypeScript is intended to assist developers in identifying issues early during compilation rather than depending exclusively on runtime checks.

Although JavaScript was identified as an emergent server-side technology by Node.js, its success at the business level is frequently hampered by the lack of robust type checking and compile-time error checks in JavaScript code bases. TypeScript was created to fill this exact gap.

This article provides a thorough walkthrough of setting up Node.js QuickStart, establishing type definitions, and incorporating TypeScript into a solid project structure.

Backend Integration: Node.js QuickStart Setup

Since its beginning, TypeScript has supported Node.js. A few common Node.js setup procedures plus particular TypeScript configuration steps are needed to set up a fast TypeScript project for Node.js:

Setup a Node.js Project ()

Initialising the project directory and creating a package.json file are the first steps. This file is crucial for dependency management and configures the npm package.

Code: Initializing package.json

npm init -y

The package.json file created by this program is simple.

Add TypeScript

Because it is only required to convert .ts files into deployable .js files, install the TypeScript compiler as a development requirement (--save-dev).

Code: Installing TypeScript

npm install typescript --save-dev

Install Type Definitions ()

In order for TypeScript to comprehend the types and structures of code written in standard JavaScript libraries, particularly those used in runtime environments such as Node.js, declaration files (.d.ts) are necessary.

The @types system, which houses type definitions created by the community, mostly from the DefinitelyTyped, is used by TypeScript.

When working with modules like fs (File System) or process, type safety and code intelligence are ensured by the @types/node package, which supplies the declaration files required for the integrated Node.js APIs. TypeScript would protest that global variables, such as process, could not be located if these ambient declarations were missing.

Code: Installing @types/node

npm install @types/node --save-dev

With TypeScript’s developer ergonomics and safety features, you can use all of the built-in Node modules (such as import * as fs from 'fs';) thanks to this installation step. Compilers automatically resolve these @types packages, which are the typical method for obtaining typings for pre-existing JavaScript libraries, for TypeScript versions 2.x and above.

Initialize

The compilation context the logical collection of files that TypeScript will scan and analyze as well as compiler parameters are defined in the tsconfig.json file.

A Node.js backend project’s configuration frequently calls for particular output directory and module resolution parameters.

Code: Initializing and configuring tsconfig.json

npx tsc --init --rootDir src --outDir lib --esModuleInterop --resolveJsonModule --lib es6,dom --module commonjs

With just one command, the tsconfig.json file is initialised with the essential settings for a contemporary Node.js project:

  1. --rootDir src: Specifies that TypeScript files reside in a src folder.
  2. --outDir lib: Directs the generated JavaScript files to an output folder named lib.
  3. --module commonjs: Specifies that the output JavaScript should use the CommonJS module system, which is typically used by default in Node.js environments.
  4. --lib es6,dom: Defines the library files included in the compilation context. While running on a server (Node.js), you typically need es6 (or newer) for modern JavaScript features. Though dom is often included for completeness, for a pure backend Node project, you primarily require Node’s own types provided by @types/node.
  5. --esModuleInterop and --resolveJsonModule are also included for modern interoperability features.

Integrating TS into a Node Project Structure

Organising a TypeScript Node.js project by separating code from generated output is a popular and strongly advised strategy.

Recommended Folder Structure

Generally speaking, the ideal folder organisation looks like this:

Folder/FilePurpose
package.jsonProject configuration and dependency manifest.
tsconfig.jsonTypeScript compiler configuration.
src/All your files (.ts and .tsx) go here.
lib/ (or dist/)All your compiled files (.js and .d.ts) go here.
node_modules/Installed dependencies (usually ignored by Git).

Example of structure:

package/
├─ package.json
├─ tsconfig.json
├─ src/
│  ├─ index.ts   <-- code entry
│  └─ foo.ts
└─ lib/
   ├─ index.js   <-- Compiled output
   └─ index.d.ts

The rootDir option in tsconfig.json is set to src, and outDir is set to lib to enforce this separation.

Core Configuration Snippets

Assuming the steps above have been followed, the project utilizes the separation of concerns between Node.js/NPM configuration and TypeScript compilation settings.

This configuration ensures the compiler targets a recent version of ECMAScript but outputs CommonJS modules, placing the output in the lib directory.

{
    "compilerOptions": {
        "target": "es2019",
        "module": "commonjs",
        "outDir": "lib",
        "rootDir": "src",
        "strict": true,
        "esModuleInterop": true,
        "resolveJsonModule": true,
        "skipLibCheck": true, // Recommended for speed
        "lib": [
            "es2019"
        ]
    },
    "include": [
        "src/**/*"
    ]
}

To simplify the development workflow and streamline deployment, custom scripts are often added to package.json.

Code: Adding build scripts to package.json

{
  "name": "ts-node-backend",
  "version": "1.0.0",
  "description": "TypeScript backend project",
  "main": "lib/index.js",
  "scripts": {
    "build": "tsc -p .",
    "start": "node lib/index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0"
  }
}

The "build": "tsc -p ." script invokes the TypeScript compiler (tsc), reading the project configuration (-p . refers to tsconfig.json in the current directory), and generating JavaScript files in the lib output directory.

Bonus: Live Compile + Run for Development

Packages like nodemon and ts-node are frequently used for a more seamless development process.

  • ts-node: TypeScript files can now be run directly in Node.js without the requirement for tsc precompilation thanks to ts-node. The code is transpiled dynamically.
  • nodemon: Whenever a file is altered, nodemon, which monitors file changes, runs a designated command (such as calling ts-node).

Code: Installing live development tools

npm install ts-node nodemon --save-dev

To manage this live execution, package.json can be modified to include a complex start script:

Code: Live development script in package.json

"scripts": {
  "build": "tsc -p .",
  "start": "npm run build:live",
  "build:live": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts"
},

When you run npm start, nodemon reruns its command (ts-node) whenever files in src/**/*.ts change. ts-node then transpiles the TypeScript, picking up the settings from tsconfig.json automatically, and runs the output JavaScript through Node.js.

Code Example demonstrating Backend Integration

This example demonstrates using the Node.js built-in fs (File System) module, relying on the types provided by @types/node.

Create File ()

We use import * as fs from 'fs' which relies on the ambient declarations from @types/node to provide type information for the fs module.

// src/index.ts

import * as fs from 'fs';

const filename: string = 'output.txt';
const content: string = 'Hello from TypeScript Node.js Backend!';

// Function using the writeFile API (provided by Node types)
function writeToFile(file: string, data: string): void {
    fs.writeFile(file, data, (err) => {
        if (err) {
            // TypeScript provides type checking for 'err' (Error type)
            console.error('Failed to write file:', err.message);
            return;
        }
        console.log(`Successfully wrote to ${file}`);

        // Try to read the file back
        fs.readFile(file, 'utf8', (readErr, readData) => {
            if (readErr) {
                console.error('Failed to read file:', readErr.message);
                return;
            }
            // TypeScript ensures readData is handled correctly
            console.log('Read content:', readData);
        });
    });
}

writeToFile(filename, content);

// Example of type safety failure (if strict mode is enabled)
// fs.writeFile(123, 456, () => {}); // <--- This line would cause a type error!

Run in Development Mode

Assuming the live development scripts are set up in package.json, running the development command executes src/index.ts directly.

Code: Executing the start script

npm start

Output:

[nodemon] starting `ts-node src/index.ts`
Successfully wrote to output.txt
Read content: Hello from TypeScript Node.js Backend!

Output (File System): A file named output.txt is created in the root directory containing:

Hello from TypeScript Node.js Backend!

Compile for Production

The TypeScript compiler converts src/index.ts into standard JavaScript, placing the output in lib/index.js as configured by outDir.

Code: Executing the build script

npm run build

Output (File System – Content of lib/index.js): The generated JavaScript is typically clean and equivalent to the original runtime logic, with all TypeScript type annotations removed.

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const filename = 'output.txt';
const content = 'Hello from TypeScript Node.js Backend!';
// Function using the writeFile API (provided by Node types)
function writeToFile(file, data) {
    fs.writeFile(file, data, (err) => {
        if (err) {
            // TypeScript provides type checking for 'err' (Error type)
            console.error('Failed to write file:', err.message);
            return;
        }
        console.log(`Successfully wrote to ${file}`);
        // Try to read the file back
        fs.readFile(file, 'utf8', (readErr, readData) => {
            if (readErr) {
                console.error('Failed to read file:', readErr.message);
                return;
            }
            // TypeScript ensures readData is handled correctly
            console.log('Read content:', readData);
        });
    });
}
writeToFile(filename, content);

Run Production Code

The production code is run directly by the Node.js runtime.

Code: Executing the production build

npm run start

Output (Console):

Successfully wrote to output.txt
Read content: Hello from TypeScript Node.js Backend!

With this thorough configuration, development may take use of quick iterations with ts-node and nodemon, while deployment can use the reliable, type-checked JavaScript output produced by tsc.

Index