Strongly-Typed Node.js applications

Using Typescript with Node.js

Enable the power of types in your Node.js application with Typescript

Andre Lopes
Level Up Coding
Published in
7 min readNov 28, 2023

--

Image from OctoPerf

Hey people!

Let’s explore the powerful combination of TypeScript and Node.js for building robust and maintainable server-side applications.

The article delves into the benefits of using TypeScript, a statically typed superset of JavaScript, in the Node.js environment.

It covers key concepts, configuration, and practical examples to help developers harness the full potential of this dynamic duo, making the development process more efficient and codebases more resilient.

Whether you’re a seasoned developer or just starting with Node.js, this article serves as a valuable guide to elevate your coding experience and enhance the scalability of your projects.

Let’s get started

To start, let’s initialize our NPM project with:

npm init -y

You can press ENTER to go through the configuration steps.

Now, let’s first install typescript in our project to enable the tsc command:

npm install --save typescript

Now, let’s set up the npm script to enable running the typescript commands. Add the tsc command to your package.json scripts:

{
"name": "typescript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"tsc": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"typescript": "^5.2.2"
}
}

With this, we can run the following command to initialize our TypeScript project and generate our configuration file tsconfig.json :

npm run tsc -- --init

This will generate a tsconfig.json file for you:

{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

If you’d like to have it with some pre-configuration, you can pass some options to the init command. Below is the one I generally use:

Linux:

npm run tsc -- --init --target esnext --module nodenext \
--moduleResolution nodenext --rootDir src \
--outDir dist --noImplicitAny --noImplicitThis --newLine lf \
--resolveJsonModule

Windows:

npm run tsc -- --init --target esnext --module nodenext `
--moduleResolution nodenext --rootDir src `
--outDir dist --noImplicitAny --noImplicitThis --newLine lf `
--resolveJsonModule

This will generate the following file:

{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"rootDir": "src",
"moduleResolution": "nodenext",
"resolveJsonModule": true,
"outDir": "dist",
"newLine": "lf",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"skipLibCheck": true
}
}

For a complete list of the compiler options, check here.

tsconfig.json

The tsconfig.json file has the configurations that the typescript compiler will use when generating your build files.

Once you run the tsc command, it initializes it with the compiler options, but it also has some other configurations aside from compilerOptions that you can customize to your needs. These are:

  • files — Tells the compiler which files are part of your build. It is mostly useful when you have just a few files in your project.
  • include — Tells the compiler which files/patterns should be included in the build
  • exclude — Tells the compiler which files/patterns should be excluded from the build
  • extends— Tells which tsconfig.json file to use as a reference. It will give you the configurations from it. (we’ll talk about it later, as it is super useful)
  • watchOptions — Configures the behavior of the tsc --watch command.
  • references — Lets you split your tsconfig.json file into multiple pieces and reference it in a central one.

If you have set rootDir in the compileOptions, files , includes , and excludes will take files relative to that directory. If not, it takes the files relative to the directory the tsconfig.json file is on.

For a complete look at all the configurations, check the tsconfig documentation here.

Using existing typescript configurations

As mentioned before, TypeScript has the possibility of letting you use someone else's existing configuration in your project.

For that, you can make use of the extends by passing the path of the configuration file you want to use. It can be relative to your project or a URL reference.

Like:

{
"extends": "./configs/tsconfig",
"compilerOptions": {
"strictNullChecks": false
}
}

Or you can use a TSConfig base, like:

{
"extends": "@tsconfig/node12/tsconfig.json",
"compilerOptions": {
"preserveConstEnums": true
},
"include": ["src/**/*"]
}

For a list of the recommended bases, check the documentation here or in this repository.

Getting into action

So, now that we know how to configure typescript, let’s set up a small project.

First, create a folder src that will hold our code. This is the same folder I defined in rootDir. Now add a index.ts file. You’ll be able to see that you can use and see types.

Number type
Custom type

Cool! We managed to enable and use typescript to write our code, but how do we run it?

To do so, you can run npm run tsc, which will compile the code into JavaScript. If you define the outDir options in the compilerOptions, the command will send the JavaScript files in there.

Project compilation

Now I can just run node dist/index.js and it will run my index.js file

Running the project

Optimizing it

Now that we know how to run it, we can optimize the build steps.

You notice that if you add other files and recompile it, they will be added to your build folder, but if you delete one, it won’t be deleted once you run npm run tsc. To fix this, let’s add the package rimraf to assist us in deleting the old files.

npm i -D rimraf

And then, we create a dedicated build step in our package.json :

{
"name": "typescript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"tsc": "tsc",
"build": "rimraf dist && tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"typescript": "^5.2.2"
},
"devDependencies": {
"rimraf": "^5.0.5"
}
}

Now, when you run npm run build , it will automatically delete the old files before generating new ones.

Start

And you can add the start script to automatically run your code:

{
"name": "typescript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"tsc": "tsc",
"build": "rimraf dist && tsc",
"start": "npm run build && node dist/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"typescript": "^5.2.2"
},
"devDependencies": {
"rimraf": "^5.0.5"
}
}

We can optimize the start step even more by using the package ts-node. It will allow you to run TypeScript code without the need to build it first.

So let’s install it with:

npm i -D ts-node

And then let’s modify the start script:

{
"name": "typescript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"tsc": "tsc",
"build": "rimraf dist && tsc",
"start": "ts-node src/index.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
"typescript": "^5.2.2"
},
"devDependencies": {
"rimraf": "^5.0.5",
"ts-node": "^10.9.1"
}
}

Then run npm start and see your code running.

Watch

There are many ways that you can enable watch in your TypeScript code.

My preferred way of doing it is by using a combination of ts-node and nodemon.

You can install nodemon with:

npm i -D nodemon

Then create a nodemon.json file in the root level and add the following code to it:

{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "ts-node ./src/index.ts"
}

Now, you add a watch script in your package.json file:

{
"name": "typescript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"tsc": "tsc",
"build": "rimraf dist && tsc",
"start": "ts-node src/index.ts",
"watch": "nodemon"
},
"author": "",
"license": "ISC",
"dependencies": {
"typescript": "^5.2.2"
},
"devDependencies": {
"nodemon": "^3.0.1",
"rimraf": "^5.0.5",
"ts-node": "^10.9.1"
}
}

This will allow you to run npm run watch and see something like the following:

Third-party libraries support

If you want to use a third-party library, like express, chances are that the team also made available a package with the types for that package.

So, if you are seeing an error like the below:

For express, you can do

npm i -D @types/express

This will add express types to your project.

{
"name": "typescript",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"tsc": "tsc",
"build": "rimraf dist && tsc",
"start": "ts-node src/index.ts",
"watch": "nodemon"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"typescript": "^5.2.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"nodemon": "^3.0.1",
"rimraf": "^5.0.5",
"ts-node": "^10.9.1"
}
}

And now you can freely use TypeScript with express.

Conclusion

In a nutshell, this article is like a friendly guide to using TypeScript and Node.js for building strong and easy-to-maintain server-side applications. It’s perfect for both experienced developers and those just starting with Node.js.

The guide takes you through the whole process, from starting a new NPM project to configuring TypeScript with a tsconfig.json file. It even throws in some cool tricks like optimizing build steps using rimraf and making your life easier with ts-node so you can run TypeScript code without the hassle of building it first.

The article doesn’t stop there — it shows you how to set up a watch mode for automatic restarts during development, making your workflow super smooth. And hey, it even covers setting up TypeScript with popular libraries like Express, showing you how to handle third-party goodies like a pro.

So, if you want to level up your coding game, make your projects more scalable, and have a blast with TypeScript and Node.js, this guide is your go-to buddy.

Happy coding! 💻

--

--