Getting started with TypeScript for Node

A step by step guide

TL;DR

Using TypeScript for Node is pretty easy to setup.

Check out a minimal setup in this repo, or read the article for a step by step guide:

Intro

This article is about a minimal setup to use TypeScript for Node. At the end we will have written a super basic app that can be used as a starting point for something bigger.

This is not an article about why to use TypeScript.

I wanted to write this article because it took me a couple of hours to set this all up. When I want to do this again I can fall back to this article and along the way I might help someone who has a similar problem.

Prerequisites

Basic JavaScript and Node are needed to follow this guide. There is also some Docker at the end.

I use VSCode to write my code and I had nvm and docker installed before I started.

Let’s go!

Let’s start by whipping open a terminal and get a hello-world express app in plain JavaScript going.

echo "12" >> .nvmrc

nvm use

npm init -y

npm install express

mkdir src

touch src/index.js

The index.js file should look something like this:

const express = require("express"); const app = express();

const port = 3000; app.get("/", (req, res) => res.send("Hello World!")); app.listen(port, () => console.log(`Example app listening on port ${port}!`));

In package.json we should add a script to start the app.

"scripts": {

"start": "node src/index.js"

}

We can now start the app and browse to http://localhost:3000.

npm start

This should show an empty page with Hello World! .

Move to TypeScript

Using TypeScript has become very easy since Babel supports it. Babel can take TypeScript code and turn it into plain JavaScript that Node understands. The downside is that Babel does not actually check the types. But we’ll handle that later.

First back to our terminal.

npm install --save-dev @babel/cli @babel/core @babel/node @babel/preset-env @babel/preset-typescript

touch .babelrc

mv src/index.js src/index.ts

The .babelrc file should look like this:

{

"presets": ["@babel/preset-env", "@babel/preset-typescript"]

}

Now we can use babel-node instead of Node to run our app, so let’s update the start script.

"scripts": {

"start": "babel-node src/index.ts --extensions \".ts\""

}

We can now start the app again and browse to http://localhost:3000. We should see the same result as before.

npm start

Let’s try and add a little TypeScript to our code just to see if it will work. I marked the changes in bold.

import express from "express"; const app = express();

const port = 3000; const sayHelloTo = (name: string): string => `Hello ${name}!`; app.get("/", (req, res) => res.send(sayHelloTo("World"))); app.listen(port, () => console.log(`Example app listening on port ${port}!`));

This code still works, that means Babel is doing a good job removing the types from the code.

If we want, we can change "World" to 1 and the code will still work. Your editor might complain that 1 is not a number but Babel doesn’t care.

We’ll fix that in the next step.

Make it strict

To actually do some type-checking we need the TypeScript compiler.

You guessed it, terminal time:

npm install --save-dev typescript

touch tsconfig.json

Fill the tsconfig.json with the following content:

{

"compilerOptions": {

"strict": true,

"esModuleInterop": true

},

"include": ["src/**/*"]

}

Now let’s add a script for running the TypeScript compiler. We just want to use it to check our code, not emit JavaScript, so we’ll add the --noEmit flag.

"scripts": {

"start": "babel-node src/index.ts --extensions \".ts\"",

"tsc": "tsc --noEmit"

}

If we run our new script we can see some errors.

npm run tsc

The TypeScript gives us a hint on how to fix them so let’s do that. If we run it again the errors should be gone.

npm install @types/express

npm run tsc

Now we have a Node application written in TypeScript. Let’s try and create a production-like environment to test it out.

Test in production

To create a production version of our app we’ll first add a build step that generates JavaScript code from our TypeScript code. We’ll make sure it stops when it encounters bad stuff.

"scripts": {

"start": "babel-node src/index.ts --extensions \".ts\"",

"tsc": "tsc --noEmit",

"build": "npm run tsc && babel src -d dist --extensions \".ts\""

}

If we run our build script, we can see it generates 1 JavaScript file in the dist folder.

Now we can create a Docker container to be sure this generated JavaScript file works as expected.

touch .dockerignore

touch Dockerfile

The .dockerignore file makes sure we don’t copy too much stuff to our image, just like a .gitignore file.

node_modules

dist

The Dockerfile is a bit more complicated. There are 2 steps, build and web.

The build step copies all files from local disk to the Docker image. It then installs all dependencies and builds our app. Finally it copies the package.json and package-lock.json files to the dist folder because we need them in the next step.

The web step takes the dist folder from the previous step. It installs only the dependencies, not the devDependencies using npm install --only=prod . And then it runs our app.

FROM node:12-alpine AS build WORKDIR /app ADD . /app/ RUN npm install \

&& npm run build RUN cp -r package.json dist/ \

&& cp package-lock.json dist/ FROM node:12-alpine AS web WORKDIR /app COPY --from=build /app/dist /app RUN npm install --only=prod EXPOSE 3000 CMD [ "node", "index.js" ]

Now all we have to do is build the Docker image and run it.

docker build -t node-typescript .

docker run --rm -p 3000:3000 node-typescript

Browsing to http://localhost:3000 should once again give the same result.

Success!

Extra pretty!

What’s better than working code?

Pretty code. So as an extra let’s add Prettier and Eslint to our project.

npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-prettier prettier

echo {} >> .prettierrc

touch .eslintrc

Our .eslintrc file should look like this:

{

"parser": "@typescript-eslint/parser",

"extends": [

"plugin:@typescript-eslint/recommended",

"prettier/@typescript-eslint",

"plugin:prettier/recommended"

]

}

Now we can add scripts for Eslint and Prettier. Eslint will check the structure of our code and also check if our code is formatted correct. We will also add the Eslint script to our build step, this way we can’t deploy code that doesn’t conform to our standards.

"scripts": {

"lint": "eslint '*/**/*.ts'",

"prettier:write": "prettier --write src/**/*.*",

"start": "babel-node src/index.ts --extensions \".ts\"",

"tsc": "tsc --noEmit",

"build": "npm run tsc && npm run lint && npm run && babel src -d dist --extensions \".ts\""

}

Now we can check if our code is up to standards by using our scripts and even auto-format it using prettier:write .

You should also have a shortcut in your editor to format using Prettier or format on save.

You can go on step further and add a pre-commit hook to format your code when you commit. Someone wrote a Medium article about this: Using Prettier and husky to make your commits safe (spoiler, it was me).

Conclusion

Using TypeScript in a Node project is not hard at all. So better start sooner than later.

Check out the full code at: