Start a TypeScript Node Project

Minimal selection of tools and libraries for a TypeScript Node project.


This setup has been tested with node v18.16.0, npm 9.7.1, eslint 8.45.0, typescript-eslint 6.0.0, prettier 3.0.0. Basic knowledge of npm is assumed. Reading reference documentation for corresponding tools is recommended.

Some options are not standard and disabled by default. I consider those options to be useful and enable them in my projects.

Create a new project

We will use a minimal package.json with ESM modules.

% mkdir my-project
% cd my-project


  "private": true,
  "type": "module"

Check JSON syntax:

% npm install

Add and configure TypeScript

@tsconfig/node18 is a tiny package which provides some default values for tsconfig to extend from. It’s recommended by typescript documentation. Alternatively we can just copy its contents into tsconfig.json to avoid extra dependency.

@types/node provides type definitions for Node.js API.

We added the exactOptionalPropertyTypes option to make TypeScript more strict about optional properties. It is a recommended option that is not enabled by default.

% npm install --save-dev 'typescript' '@tsconfig/node18' '@types/node'


  "extends": "@tsconfig/node18/tsconfig.json",
  "include": ["src"],
  "compilerOptions": {
    "exactOptionalPropertyTypes": true,
    "rootDir": "src",
    "outDir": "dist",
    "verbatimModuleSyntax": true

Now we can add two files in order to test our setup.

Please note that we’re using .js file extension in import statement in the .ts file. This is not a typo or error. That’s how TypeScript works with ESM.


export function concat(a: string, b: string): string {
  return a + " " + b;


import { concat } from "./concat.js";
console.log(concat("Hello", "world!"));

Now we will compile, inspect results and run the code. Notice that JavaScript output is pretty much identical to the TypeScript minus types.

% npx tsc


export function concat(a, b) {
  return a + " " + b;


import { concat } from "./concat.js";
console.log(concat("Hello", "world!"));
% node dist/index.js
Hello world!

Add and configure ESLint

The default ESLint configuration is a good starting point. However, there are plenty of additional checks that are not enabled by default. For instance, the eqeqeq check has been added.

% npm install --save-dev 'eslint' '@typescript-eslint/eslint-plugin' '@typescript-eslint/parser'




  "extends": [
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2022,
    "project": "tsconfig.json"
  "plugins": ["@typescript-eslint"],
  "root": true,
  "rules": {
    "curly": ["error", "multi-line"],
    "eqeqeq": "error",
    "@typescript-eslint/strict-boolean-expressions": [
        "allowString": false,
        "allowNumber": false,
        "allowNullableObject": false

Check that it works:

% npx eslint .

Add and configure Prettier

Prettier provides good defaults out of the box and even maintains a philosophy regarding its options. In this guide, we will change the trailingComma option.

% npm install --save-dev 'prettier'





Now we can check if our sources are properly formatted:

% npx prettier -c .
Checking formatting...
All matched files use Prettier code style!

Unit testing

Node.js has a built-in test runner. We will add a unit test for our concat function.


import { test } from "node:test";
import { strict as assert } from "node:assert";
import { concat } from "./concat.js";

await test("concat works", () => {
  assert.equal(concat("a", "b"), "a b");

Now we can compile and run this unit test. node --test dist will find all JavaScript files with names matching some rules inside dist directory and will execute every one of them.

% npx tsc
% node --test dist
✔ concat works (0.48175ms)
ℹ tests 1
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 45.819834

Configure npm scripts

It’s a good idea to add a few npm scripts so we won’t have to memorize all the commands and their options.


  "private": true,
  "type": "module",
  "scripts": {
    "build": "tsc",
    "build:watch": "tsc --watch",
    "check": "eslint --max-warnings 0 . && prettier --check .",
    "start": "node dist/index.js",
    "start:watch": "node --watch dist/index.js",
    "test": "node --test dist",
    "test:watch": "node --test --watch dist"

Now we can execute npm run build:watch in the background terminal and our TypeScript code will be constantly compiled as it changes. npm start will start our application and npm run start:watch will restart it on any code change. Similarly we can use npm test to run tests once or npm run test:watch to run tests on every code change.

IDE remarks

I’m using VSCode with the following extensions: ESLint, Prettier. It supports TypeScript out of the box. Here’re some settings which configure Prettier to be a default formatter and to run it on save:

  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "prettier.proseWrap": "always",

It’s possible to configure WebStorm or Idea with ESLint and Prettier as well.

Using --watch options allows for very fast feedback loops.