Migrating a Nodejs Telegram Bot to TypeScript

Hello everyone! In this post, I will continue to work on my chatbot. Lately, I've been using TypeScript a lot, and I found I'm more productive using it. I decided to migrate my chatbot project from JavaScript to TypeScript.

Hello everyone! In this post, I will continue to work on my chatbot. Lately, I've been using TypeScript a lot, and I found I'm more productive using it. However, I only had experience with projects that originally use TypeScript. So in this post, I decided to migrate my chatbot project from JavaScript to TypeScript.
Before we dive into the actual migration, I think it makes sense to present briefly the main reasons you want to use TypeScript.

  • First of all, you get a much stricter type system than JavaScript. Also, you have control over how stringent the typing system is, so if you want to prototype something quickly, you don't have to fight with the type system.
  • The tooling support for TypeScript is better in most of the modern IDEs. Thus, your workflow becomes more efficient.
  • Finally, you can compile TypeScript to whatever ECMAScript standard you need. Consequently, you can write in a modern language with all the necessary features for a better development experience and still be compatible with very old specifications.

If you want to learn more about the design goals of TypeScript you can look up this entry in the TypeScript wiki.

Migrating from JavaScript

If you are interested in migrating a project from JavaScript to TypeScript, there is a detailed guide in the TypeScript documentation. However, many of the details there are redundant because our project does not have any client-side code, so I will only present what's relevant to us.

Installing TypeScript

First of all, we need to install TypeScript. We can either install TypeScript globally:

npm i -g typescript

Or as a dev dependency in our project:

npm i -D typescript

We can also install ts-node to be able to run TypeScript files instead of building and running every time:

npm i -D ts-node

The ts-node tool will start a TypeScript REPL (Read Evaluate Print Loop).

Adjusting the project structure

If you are following my bot project you might remember the restructuring we did on the project. The project structure looks like this:

src/bot-core
src/libs
src/rest-api
main.js

We have separate folders for the main business logic of the bot under bot-core, the third-party integrations under libs, and finally, the server logic that exposes the API under rest-api. The main.js file is responsible for bootstrapping the server and starting everything. However, now we need to separate the source code from the compiled code. A clean way to do this is to have a separate folder. Common names for this folder are built, dist or something along those lines. I named the output directory dist, thus the new structure will look like this:

src/bot-core
src/libs
src/rest-api
src/main.js
dist/

Initialize TypeScript project

If you installed TypeScript on a project level you need, run the command below to initialize your project:

./node_modules/.bin/tsc --init

This command will generate a tsconfig.json with the default configuration.

The tsconfig.json holds all compiler options and specifies which files to include or exclude from the compilation process. An interesting feature of the tsconfig.json is that it supports inheritance, so you can have a root configuration file and then different files for different environments. You can find a detailed reference in the documentation

The generated tsconfig.json contains only one property in the root level, the compilerOptions. As the name explicitly states, it is the object containing all the possible compiler options you can set. You can also lookup the documentation on compiler options to find what is useful for your case.

However, I can imagine that this might be a tad overwhelming, so I will try to present to you what, in my opinion, are the essential compiler options and what flags you should configure.
Below is the basic tsconfig.json:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "allowJs": true,
    "outDir": "./dist",
    "sourceMap": true,
  },
  "include": ["./src/**/*"],
}
  • The include property lists all the directories that we want to include in the compilation.
  • If we're going to exclude parts of the files that we contain in the include list, we can use the exclude keyword.
  • The outDir specifies the output directory for the compiled files.
  • The module property defines the type of JavaScript modules we want to use in the compiled code.
  • The target property defines the version of the ECMAScript specification you want to support in the compiled code. Since we don't have any specific constraints, I picked es6.
  • Setting the allowJs to true allows us to have JavaScript files in the source directory. This setting is handy when you want to migrate big projects since you don't want to refactor the whole project at once. You can refactor parts of it to TypeScript and leave the rest in JavaScript. The same thing happens here where I don't want to do the whole refactoring in the scope of this post, but I want to prepare it for migration to TypeScript.
  • Finally, the sourceMap property tells the compiler to create a mapping between the input and output files of the compilation process. Since all the stack traces will come from the compiled code, it might be difficult to map them manually to the source files. By having a generated map and by using the --enable-source-maps nodejs flag, we have stack traces with the lines of both the source and the built file where the error occurred. If you want to read more on the subject of source maps, you could take a look at this post.

Deployment on Heroku

The deployment process on Heroku is straightforward. By default, Heroku will run your build script, so all we have to do is create a build script that will compile our project. Then we also need to change the starting scripts to use the compiled code. Below you can see how the scripts in package.json should look like:

"scripts": {
    "build": "tsc",
    "start": "node --enable-source-maps -r dotenv/config dist/main.js",
    "start:prod": "node --enable-source-maps dist/main.js"
}

In the Procfile, we need the following command:

web: npm run start:prod

Summary

To summarize, in this post we briefly presented how TypeScript can improve the Javascript development experience. Moreover, I can argue that we successfully migrated the Telegram chatbot to Typescript even though we didn't write a single line of Typescript yet. To keep this short, I will continue with translating javascript into TypeScript in an upcoming post.
I hope you found this post informative! Stay tuned!

Subscribe to Backend Definite

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe