Set up a Node.js project with TypeScript, ESLint, and Prettier

Photo by Steve Harvey on Unsplash
Photo by Steve Harvey on Unsplash

TypeScript is a free and open-source programming language developed by Microsoft to improve and secure JavaScript code production.

It is a superset of JavaScript, meaning any correct JavaScript can be used with TypeScript. Its code is transpiled in JavaScript and can be interpreted by any Web browser or JavaScript engine. The first version was released in 2012.

Why do you need TypeScript?

TypeScript brings more comfort in syntax writing. It's a good choice to use it because :

  • An optional static type of variables and functions
  • Object-oriented programming support
  • Module import
  • It supports the ECMAScript 6 specification.
  • The ability to compile down to a version of JavaScript that runs on all browsers.
  • IDE provides IntelliSense, which improves the developer experience.
  • The code is easier to test.

Prerequisites

To follow this tutorial, you must install these tools:

  • Node.js 20.6 or higher - Download's link
  • A Node.js package manager such as NPM or Yarn.

Node.js comes with NPM, which can be used to install Node.js modules, so feel free to use it. I will use Yarn.

Initialize node project

Create a new folder that will hold the contain our source code:


mkdir node-typescript-starter

cd node-typescript-starter

We are inside the folder; let's initialize a Node.js project by running the command below:


yarn init

The CLI will ask questions about your project; answer as you want. Here is the project initialization example:

Initialize a Node.js project
Initialize a Node.js project.

A file named "package.json" will be created, the configuration file for a Node.js project.

Install TypeScript

Run the commands below to install TypeScript:


yarn add typescript
yarn add -D @types/node

The first line installs TypeScript in the project, and the second line installs the types definition of the Node.js native modules such as http, fs, path, cluster, etc...

💡
The CLI option -D the second command indicates that we want to install this package only for development, so it will not be installed if we are in production mode.

Let's set up TypeScript's configuration, which consists of a file with rules TypeScript uses to perform type checks and transpile the code to JavaScript.

For example:

  • Which version of JavaScript we are targeting?
  • Which directories should be excluded or included in type checking?
  • Do you want to enable/disable some type-check rules?
  • Etc...

Run the command below to initialize the TypeScript configuration:


yarn tsc --init
# or
npx tsconfig.json

These two commands have the same goal but produce different results.

  • The first command generates a "tsconfig.json" file with comments on each property, which helps understand its purpose. I recommend this command to someone who is just starting with TypeScript.
  • The second command generates a "tsconfig.json" file with many predefined properties already configured, contrary to the first, which has few properties. I recommend this for someone already familiar with TypeScript.

You can customize the "tsconfig.json" as you want. I will provide the one I use on all my projects in the project repository at the end.

Write code with TypeScript

Create an "src" folder, then create a file named "index.ts". Feel free to use your favorite editor or IDE.


mkdir src
cd src
touch index.ts

Open the file "index.ts" and paste the code below:


const addition = (a: number, b: number): number => {
    return a + b;
};

const number1: number = 5;
const number2: number = 10;
const result: number = addition(number1, number2);

console.log('The result is %d', result);

Run the Node.js project

We wrote our super function, which calculates the sum of two numbers, and we want to run it to see the output. Let's look at a possible way of running the project.

1. Transpile the file and run it with the Node.js

The Node.js runtime runs only JavaScript code; since ours is in TypeScript, we must convert it to JavaScript and then run it. The command below achieves that:


yarn tsc

node dist/index.js

The output will be similar to the picture below:

Transpile the TypeScript file and run the JavaScript output with Node.js
Transpile the TypeScript file and run the JavaScript output with Node.js.

2. Run the Typescript file directly

We can run our code but must run two commands to see the result. Why can we just execute our .ts file directly? Also, we only need JavaScript files to deploy them in production.

Fortunately, it's possible to run TypeScript files using a TypesScript runtime such as TS Node, Tsx, Jiti, Sucrase, etc... Here is a features comparison between major TypeScript runtimes.

We will use TS Node, so let's run the command below to install it.


yarn add -D ts-node 

Run the command below to execute the TypeScript code:


yarn ts-node src/index.ts

The output will be similar to the picture below:

Run a TypeScript file with TS Node.
Run a TypeScript file with TS Node.

3. Run the file on changes

To improve the developer experience while coding, running the code when a file changes is great as it will increase the feedback loop. We can use a file watcher to detect file changes in the project and run the command to run the project.

A Node.js package called Nodemon is suited for this task, so let's install it.


yarn add -D nodemon

Before you start editing your files, run this command:


nodemon --watch "*.ts" --exec "ts-node" ./src/index.ts

This command means: Watch every change on all files having the .ts extension, then use ts-node to run the project and use the file ./src/index.ts as the entry point.

💡
If you want to add another file to watch, for example, a .json file, add another CLI option --watch followed by ".json"

nodemon --watch "*.ts"  --watch "*.json" --exec "ts-node" ./src/index.ts

Load environment variables

In a real-world application, we load sensitive information such as database connection URL, API key, and server SSH key from the environment variables.

Supporting it out of the box will be great for the starter project. Let's say we want to read the application name from the environment variable "APP_NAME" and display it.

In the project root folder, create a file named ".env"


touch .env

Open it and add the content below:


APP_NAME=node-ts-starter

Update the file "src/index.ts" to read and display the environment variables


const addition = (a: number, b: number) => {
  return a + b;
};

const number1 = 5;
const number2 = 10;
const result = addition(number1, number2);

console.log(`The application name is "${process.env.APP_NAME}"`);

console.log('The result is %d', result);

Starting version 20.6.0, Node.js supports environment variables, so we don't need to install a Node.js package. If you are still under version 20.6.0, I recommend using dotenv.

The Nodemon command is the following:


nodemon --watch "*.ts" --exec "node -r ts-node/register --env-file=.env" ./src/index.ts

We replaced "ts-node" with "node -r ts-node/register --env-file=.env" which tells Node.js to preload "ts-node/register" which is helpful to execute a TypeScript file in Node.js; then, load environment variables in the ".env" file.

Run the command on your computer, you get the following output:

Load environment variables when running a Node.js application.
Load environment variables when running a Node.js application.

You can see the application's name is read and displayed. To learn more about environment variables in Node.js, check out my post below.

Read environment variables in a Node.js application
This post shows how to load and read environment variables in a Node.js application written in JavaScript first and then in TypeScript. We will see how to validate the environment variables at the application launch.

Configure ESLint and Prettier

ESLint helps write better code by checking if the code is well-written and well-formatted and respects some rules defined as the best practices for maintainable and consistent code.

Let's install the required dependencies:


yarn add -D eslint prettier eslint-config-prettier eslint-plugin-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser

Create a file .eslintrc.js in the root folder of the project and add the code below:


module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'plugin:prettier/recommended',
    'prettier',
    'eslint:recommended'
  ],
  plugins: ['@typescript-eslint'],
  parserOptions: {
    ecmaVersion: 2022,
    sourceType: 'module',
    project: 'tsconfig.json',
  },
  env: {
    es6: true,
    node: true,
  },
  rules: {
    'no-var': 'error',
    semi: 'error',
    indent: ['error', 2, { SwitchCase: 1 }],
    'no-multi-spaces': 'error',
    'space-in-parens': 'error',
    'no-multiple-empty-lines': 'error',
    'prefer-const': 'error',
  },
};

The interesting part of the code above is the rules property. It is here we define our rule.

For example:

semi: 'error' means a semicolon is required at the end of an instruction.

prefer-const means the use of let is forbidden, and the use const instead.

Here is the list of available ESLint rules; feel free to customize them.

If a rule is not respected, the line will be underlined with a red or orange line depending on the error level set for this specific rule, which is "error" or "warning" respectively.

Configure Prettier

Create a file .prettierrc.js at the root folder and add the code below:


module.exports =  {
  semi: true,
  trailingComma: 'all',
  singleQuote: true,
  printWidth: 150,
  tabWidth: 2,
};

In this file, we define rules that Prettier will use to format our code for us.

semi: true means prettier would add a semicolon at the end of a line if we forgot to do that.

tabWidth: 2 means prettier will indent lines with two spaces.

Exclude files and folders from the ESLint check

In a project, some folders and files hold source code that is irrelevant to us, such as generated folders, Node.js modules, libraries configuration files, etc. We don't want to apply type checks on them.

To exclude files, let's create a file named ".eslintignore" in the project root directory and add the code below:


dist
.eslintrc.js
.prettierrc.js

We exclude the "dist" folder containing the TypeScript code transpiled into JavaScript and the configuration files for ESLint and Prettier.

By default, ESLint excludes the "node_modules" folder, so adding it to the .eslintignore is unnecessary.

Lint and format our code

Let's add commands in the "scripts" property in our "package.json" to check ESLint and Prettier errors and fix them:


...
"scripts": {
  "lint": "eslint src/**/*.ts --fix"
}
...

Let's edit the code of the src/index.ts to intentionally break some ESLint rules:


const addition = (a: number, b: number): number => {
  return a + b;
}

let number1: number = 5;
var number2: number = 10;
const result: number = addition(number1, number2);

console.log(`The application name is "${process.env.APP_NAME}"`);

console.log("The result is %d", result );



The table below lists the ESLint errors on the above code.

Line Error EsLint rules broken
Line 3 No semicolon at the end semi
Line 5 Using let instead of const prefer-const
Line 6 Using var instead of const no-var
Line 9 There is a space before the closing parenthesis space-in-parens

Run the command yarn eslint src/index.ts, you will get the following output:

Check ESLint errors in the file.
Check ESLint errors in the file.

There are four ESLint errors and three Prettier errors; run the command below to fix them:


yarn lint

All earlier errors are automatically fixed, and the concerned files are updated.

Check ESLint errors in files and fix them.
Check ESLint errors in files and fix them.
💡
Not all ESLint errors are automatically fixable; you must fix them manually to make the check succeed.

Bonus: Type Inference

TypeScript has a great feature that consists of guessing the variable type based on the value assigned to the last one or guessing the return type of a function based on the value returned.

Let's see some examples:


const isValid: boolean = true; // The type is explicit

let age = 44; // TypeScript will give the type of value '44' to the variable 'age' which is a Number

const word = "5"; // TypeScript will do the same so 'word' is a type String

number = word; // TypeScript will show and error because a String can't be assigned to a number.

We can refactor our code to remove unnecessary types. The advantage of doing that is that our code will be more readable and avoid boilerplate code.


const addition = (a: number, b: number) => { // Typescript will guess the function returns a number
    return a + b;
};

const number1 = 5;
const number2 = 10;
const result = addition(number1, number2); // Result will be a number

console.log('The result is %d', result);

We have a good project starter for building awesome Node.js applications using TypeScript.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.