Setting up TypeScript with Node

Whilst it can front-load some of the effort required to build a large-scale JavaScript application, over the last couple of years I've found TypeScript to be something that protects both you (or future you) and the people you work with the longer a project or codebase lives.

Support for TypeScript in frameworks that are built around TypeScript like Angular is pretty amazing but I've found with Node it can help to get the basics right in your head first before trying anything too complex.

What we're going to build

In the course of this tutorial, we will look to achieve the following:

  1. Setup a TypeScript project from scratch
  2. Create a basic web application with Express.js using TypeScript
  3. Create a basic build process using npm scripts.
  4. Learn to debug our application using the Node Inspector and Chrome

This article is written on the assumption you are not a complete beginner (or if you are that you're pretty determined to the point you'll fill in the gaps in your knowledge yourself) and that you have a basic knowledge of JavaScript, ideally with some knowledge of ES6, though the code we will write will be simple enough that a quick google of any unknown syntax should suffice to fill in the gaps.

The complete application can be found at This GitHub repo.

Pre-requisits

You will need the following things installed already:

  1. Node (v8 or v7+)
  2. Npm (this should come with Node though may depend on how you install Node)
  3. Any text editor that supports TypeScript.
  4. Any version of Chrome released/updated after Jan 2017

NB: I'll be working on the assumption that you chose an editor such as Vs Code that can work with TypeScript out of the box without needing to install extensions.

Getting TypeScript

First, we need to install TypeScript globally by executing the following in the command line:

npm install -g typescript

The reason we are installing it globally is so that the tsc command will be available via the command line. Though it can be used to do our TypeScript compilation we will be using it solely to initialize our TypeScript application.

Creating our TypeScript application

We now need to create a folder for our application and inside the folder run the following command line:

tsc --init

This creates a tsconfig.json file which will allow TypeScript to know how to build our TypeScript files.

The file generated will come with a lot of stuff in it. We're going to focus on understanding the most important things to know so the first thing to do is delete everything in the compilerOptions section.

Now update the compilerOptions section so that your tsconfig.json file looks like the following.

{
  "compilerOptions": {
    "target": "es5",                         
    "module": "commonjs",                    
     "lib": ["es2015"],                      
    "sourceMap": true,                       
    "strict": true,                          
     "moduleResolution": "node",             
     "typeRoots": ["node_module/@types"]   
  }
}
module

This is the module type that we want our code to generate to. This is determined by where and what will run our code. We are running on the server using Node.js so we will choose "commonjs" as this is the module format that Node uses.

moduleResolution

This determines how imports for files and libraries in our project resolve. When set to "node" then relative URLs resolve relative to our file (go figure!) whilst non-relative URLs will favor the node_modules folder which is why if we install a package like "lodash" then we can require it like so:

    import * as _ from 'lodash'; 

For a more comprehensive explanation of the algorithm used, see the TypeScript docs.

typeRoots

This lets the compiler know where to find our typings files. Without these files we would not get help from our tooling and often our code will not compile without them. The TypeScript compiler will actually look here ("node_module/@types") by default. We're just setting this for the sake of understanding the importance of typing files to our workflow.

NB: The current way (for TypeScript 2.0+ users) of installing packages is to install under the @types npm scope. So later when we get our typings they will be installed in the node_modules/@types folder. Also, libraries that are written in TypeScript (should) come with their own type definition (d.ts) files.

Target, Sourcemaps, and Strict

Target specifies the version of JavaScript that our code will be translated into. This process (Babel is another great way of doing this) is imperative in any grown up application using JavaScript these days.
If we know what version of JavaScript is guaranteed to be present where our code runs then we can rely on our code running there even if the version is much older than the version we are writing in. And which developer doesn't want to use tomorrow's features today!

Sourcemaps will become important as we are coding as it allows us to run our JavaScript whilst our tooling lets us view and debug the lines of TypeScript that relate to the JavaScript that is actually running under the hood.

The strict option enforces strict type checking in our application. Essentially the TypeScript compilation process will fail if TypeScript cannot find type information in either your code or any libraries that you import. For a more detailed explanation of this feature and how to opt into parts of it, there is a great blog post by Marius Schulz on this.

lib

This is a pretty important one to understand. This will allows us to specify type files that should be included in our build compilation process.

For example, if our executing environment supports the JavaScript "Promise" type but we have no typing files that define the "Promise" type then our code won't compile.

We are going to use the ES6 method "Object.assign" so using "es2015" will ensure all the base typings we need are there.

NB: This, however, does not mean that these types will actually be there when the application is running. (Sorry if this is getting confusing). So if we are going to be running our code in browsers (or Node versions) that don't have the "Promise" type or "Object.assign" then we still need a Polyfill even though the TypeScript compiler would be fine with our code. If you are using the recommended version of Node for this tutorial then this shouldn't be an issue.

Installing our dependencies

Now we need to get our dependencies. First, we need to initialize npm in this directory to get our package.json file which will tell npm what packages our code needs to run and what packages we use in development only. So we run the command line:

npm init

Choosing the default options will be fine. (so just press return when prompted for anything)

We are going to use Express.js so next, we will need to save that as a dependency.

npm install express --save

Next, we need to get typings for both Express and Node by running:

npm install @types/node @types/express --save-dev

Now without the Node typings, we wouldn't be able to use things such as the "console" object or other Node-specific global objects like the "process" or "__dirname". TypeScript is aware that there is a global context and, if at compile time, it sees things that it doesn't expect on a type that it is aware of, it will not compile.

We also need typings for Express. If we turned the "strict" compiler option to "false" Express would still work without typings as TypeScript would treat any unknown import as the "any" type and would simply not give us any tooling support.

Setting up our build process

Now we need to configure a build process. First, we should get a version of TypeScript for this app by running the command:

npm install typescript --save-dev

Though our globally installed version of TypeScript could build our code this is often a bad idea. We will be writing our app based on the dependencies that we install whilst developing it. If we later install a different version of TypeScript then we may get unexpected side-effects.

Fortunately, there is a very easy way to prevent this.
The "scripts" section of the package.json file we created using npm allows us to specify command lines that can be executed by using npm. e.g:

npm run {name-of-script}

When we run commands via npm scripts then the contents of the ".bin" folder in our application's "node_modules" folder will execute as if it were on our PATH variable and will link to our locally installed version of TypeScript.

Therefore if we add the following to the "scripts" section of our application's package.json:

"start": "tsc && node index.js"

the logical AND (&&) operator results in the "tsc" command (which will build our TypeScript files) being executed and once this has finished Node will execute the file "index.js" (which we are yet to write), which will launch our app.

Writing some code

Now that our setup is done, which is probably the hardest part of what we are doing, we need to create a file called "index.ts" in the root of our project.

After that we should add the following:

import  * as express from 'express';

const app: express.Express = express();

app.get('/', (req: express.Request, res: express.Response) => {

    let person = { name : 'bob' };

    person = Object.assign(person, req.query);

    res.json(person);
});

app.listen('3002',() => console.log('Server listening on port 3002'));  

This is a very simple app that exposes one endpoint at localhost:3002 that returns an object with a name property of "bob".

To start our app we simply need to run the following command:

npm start

Which will build and compile our TypeScript file to index.js then use Node to launch the application.

Should you request the page (http://localhost:3002) in a browser and put additional query string values in the URL, then we are going to use the ES6 method object.assign to update our single property object and then return it to the browser. So putting in the URL "http://localhost:3002/?name=test&age=20" will produce:

{
    "name": "test",
    "age": "20"
}

Debugging

Lastly, we want to take a look at how to debug our application. As we set the "sourcemaps" option of our compiler options to true earlier we are able to debug our TypeScript file as Node executes our source (JavaScript) files. (That's the purpose of the *.map files that get produced by our build process)

First, we need to setup another npm script in the package.json by adding:

    "debug":"tsc && node --inspect index.js"

to scripts section.

By placing the "--inspect" flag in front of the script we want to execute we are able to use the v8-inspector to debug Node.js using the chrome dev tools. (This is available from Node 6.3+). If we now run:

npm run debug

Our application will start up whilst giving us the ability to attach to the Node process using TCP.

By going to the URL "chrome://inspect" will open up:
inspector

Clicking on the link that represents our application will open up the inspector and allow use to set breakpoints in the TypeScript.

v8-inspect NB: The easiest thing to debug is the get method. If you want to debug the first few lines of code you can change our debug script to use the "--inspect-brk" flag instead of the "--inspect" flag.

Alternative

I had a few issues with stability when using Chrome to debug on windows. However, VsCode offers a great debugging experience with Node and is also able to the use the inspector protocol. When launching in the command line, it is important to take note of the port that the inspector is running on as this may well differ from the default port assumed by Vs Code.

launch debug windows

If you then go to the debug panel and add the attachment to node configuration (see below) you should be able to attach to the currently executing debug session.

debugging with vs code

Final thoughts

So we've achieved everything that we set our to do :).
The app we built was contrived to be small so that we could focus on the workflow around actual coding which is often the pain point that can make people feel that TypeScript is actually an obstacle to overcome.

As always there are other ways of enforcing a workflow to allow a codebase to scale but TypeScript is now fairly mature in terms of both resources and community which is often a critical point in the thinking of any company or developer who really cares about the team they work in.

Andrew de Rozario

Self / community educated developer who loves all things web, JavaScript and .Net related. Passionate about sharing knowledge, teaching and eating though maybe not in that order.

Subscribe to Just In Time Coder

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!