Setting up the Typescript compiler to integrate with Node.js development

; Date: Sun Jan 30 2022

Tags: Node.JS »»»» TypeScript

TypeScript is growing rapidly in popularity within the JavaScript ecosystem. TypeScript offers us important compile-time data type enforcement, for what is otherwise JavaScript code. Any large project written in JavaScript will, in theory, find this useful because, in theory, the larger the project the more likely the coders will have keeping track of data types. The TypeScript tools run on Node.js, and while it can generate code for Node.js, it can be configured for any JavaScript environment.

In this article we'll see how to set up a proper coding environment in which we can code in TypeScript on the Node.js platform.

The Typescript toolchain is written in Node.js. Of course your first step is to install Node.js and npm (and yarn if you like). You probably already have Node.js installed, but if you need to learn about installing Node.js pay attention to the following section.

Remember that the TypeScript compiler converts TypeScript code to JavaScript, and that it targets a wide range of JavaScript environments. Your individual use case might be running JavaScript-compiled-from-TypeScript in a browser. But, because the TypeScript tools require Node.js, we must still install Node.js even if we aren't targeting Node.js. In this book we're focused on running such code on Node.js, and are therefore using it both to run the TypeScript tools, and for executing our programs.

This is an excerpt from (www.amazon.com) Quick Start to using Typescript and TypeORM in Node.js applications.

This is part of a series including: (itnext.io) Choosing BetweenTypeScript vs JavaScript: Technology, Popularity

You can learn about Typescript from its website: (www.typescriptlang.org) https://www.typescriptlang.org/

Installing Node.js on your development machine

Node.js is a platform for running JavaScript applications on macOS, Windows, Linux, and several other operating systems. It allows one to run JavaScript code outside of web browsers, and therefore free's JavaScript from being dismissed as just a browser language. A long list of developer tools, application frameworks, and more, are available for the Node.js platform.

The installation process varies from one platform to another, depending on whether there is a package manager and certain details about its use. That is, one method for installing Node.js is to go to the download page ( (nodejs.org) https://nodejs.org/en/download/) and download the installer for your operating system. But it is better to use a package management system if only to simplify updating it when a new release is issued.

For modern versions of Windows, run this command:

PS C:\...> winget search nodejs
PS C:\...> winget install OpenJS.NodeJS.LTS

The first command shows you the available packages, and the second installs the current LTS version of Node.js.

WinGet is an official package/application manager for Windows. Another popular package manager for Windows, Chocolatey, is also popular. See: (chocolatey.org) https://chocolatey.org/

For macOS using MacPorts ( (www.macports.org) https://www.macports.org/), run these commands:

$ sudo port search nodejs
$ sudo port install nodejsNN

The first command shows you the available packages, for example nodejs14 or nodejs16. For the second command, substitute for NN the version number you desire. As of this writing, nodejs16 is the latest production version, and development on nodejs17 has just begun.

For macOS using HomeBrew ( (brew.sh) https://brew.sh/), run these commands:

$ brew search nodejs
$ brew install node@NN

The first command shows you the available packages. For HomeBrew, the naming convention is to use @NN to specify the version number.

The Node.js website has instructions for many more operating systems at: (nodejs.org) https://nodejs.org/en/download/package-manager/ For most Linux distributions, Node.js is available through the package management system.

Once you've installed Node.js, you can verify it by running these commands:

$ node --version
$ node --help

They print out useful information, and act to verify that Node.js is installed and running.

The above is useful for most purposes. But sometimes you need to easily switch between Node.js versions, for example to verify a bug against a specific Node.js release. For that purpose consider one of these tools:

The first, nvm, is a CLI tool for POXIX-like systems (macOS, Linux) to manage Node.js installation. It claims to run on Windows using GitBash or Cygwin, or in the WSL (Windows Subsystem for Linux) environment. The other tools are explicitly made for Windows, to the same idea as nvm.

With these tools it's easy to install the latest Node.js version, to keep several versions installed, and to easily switch between Node.js versions.

Installing TypeScript as a global tool

You will learn from the Typescript quick start guide that one installs Typescript as so:

$ npm install -g ts-node typescript

The typescript package is recommended to be installed globally (the -g option). It installs the commands, tsc and ts-node, that are the Typescript compiler. The compilers generate JavaScript source from Typescript files. It is the JavaScript that will be executed, of course, and is what will be deployed to browsers or as Node.js modules.

But, for Node.js development the recommended best practice is to list all dependencies in package.json. A global installation violates that best practice, and means that while most project dependencies are installed with package.json you have to remember to install other dependencies in addition.

Installing TypeScript in a Node.js project directory

As we just said, it's more seamless to install all dependencies using package.json. With that in mind, lets see how to set up a Node.js project directory, along with a local TypeScript installation.

We're not going to save it to the regular dependencies because TypeScript tools shouldn't be used in production environments. Instead we'll add it to devDependencies where it will be used in a development environment.

Since the goal is using TypeScript in a Node.js project, let's initialize a blank project directory:

$ mkdir project-name
$ cd project-name
$ npm init    (or yarn init)
... answer questions

This gives you a project directory with a package.json. You'll need to install other tools, set up code you're interested in, etc. But, to install TypeScript in such a directory, run this:

$ npm install typescript ts-node --save-dev

added 16 packages, and audited 17 packages in 4s

found 0 vulnerabilities

$ ls node_modules/.bin/
acorn      tsc  ts-node  ts-node-cwd  ts-node-script  ts-node-transpile-only
ts-script  tsserver

This is a local install, and by using --save-dev the dependency is added to the devDependencies section of package.json. The devDependencies are not installed when npm install is run in production mode, meaning either the --production flag is set or the NODE_ENV environment variable is set to production.

The TypeScript tools are installed in node_modules/.bin. There are two ways to conveniently run those tools. The first is to use npx like so:

$ npx tsc --version
Version 4.5.5

$ npx tsc --help
tsc: The TypeScript Compiler - Version 4.5.5

COMMON COMMANDS

  tsc
  Compiles the current project (tsconfig.json in the working directory.)

The npx tool comes packaged with recent versions of Node.js. One purpose is to dynamically download global installed tools with no fuss, the other is to assist with running locally installed tools like this.

Another method is to add ./node_modules/.bin to ones PATH variable like so:

$ export PATH="./node_modules/.bin:${PATH}"

This is a little trick to configure the shell to look for installed commands in the node_modules directory in addition to the regular commands in the PATH.

Of the tools which were installed, pay most attention to tsc and ts-node. The first is the TypeScript compiler which you'll use to generate JavaScript code from TypeScript. The other is a tool for directly running TypeScript code without (seemingly) having to compile it first.

In package.json you'll find this:

"devDependencies": {
    "ts-node": "^10.4.0",
    "typescript": "^4.5.5"
}

Remember that this means the TypeScript compiler won't be installed in production environments.

Writing a sample application in TypeScript

We're focusing most on project setup here, which means we need to write a little bit of code. If you want to setup TypeScript support in your IDE, there is a section towards the bottom discussing what to do.

Let's create a file, encode.ts, which will encode files using the UUEncode format.

import { encode } from 'uuencode';
import { promises as fsp } from 'fs';

async function encodeFile(fn : string) {
    const text = await fsp.readFile(fn);
    return encode(text);
}

(async () => {
    console.log(await encodeFile(process.argv[2]));
})();

UUEncode is from the dark ages of the 1980's when UUCP was the primary tool for shipping files from one location to another. You may some day need to send files using this format, as I had to do in the 1980's.

We would write almost identical code using straight Node.js. We read the file using readFile, using the async/await coding style to make this easier. We then encode the text, then print it to the console.

In current Node.js (starting with v 14.8), top level use of the await keyword became supported in ES6 modules. I was unable to work out how to do the same with TypeScript. Therefore, the await keywords were stashed inside async functions. The last three lines are an inline async arrow function, so the script can call encodeFile in a clean way.

To support this fine program you must install the uuencode package:

$ npm install uuencode @types/uuencode @types/node --save

This installs not only the package, but the Types package. In the TypeScript world, a Types file is a concise declaration of the data types and functions exported by a package. In the best of worlds every package would host their own types files, and we wouldn't have to install a second package like this. Here in the real world, not every Node.js package maintainer has the wherewithall to generate the types file for their project, and instead the Definitely Typed project performs this service for them.

To search for npm packages containing TypeScript type definitions, use the search engine: (www.typescriptlang.org) https://www.typescriptlang.org/dt/search/

To simply run this script:

$ npx ts-node encode.ts package.json 
M>PH@(")N86UE(CH@(G1S(BP*(" B=F5R<VEO;B(Z("(Q+C N,"(L"B @(F1E
M<V-R:7!T:6]N(CH@(B(L"B @(FUA:6XB.B B:6YD97@N:G,B+ H@(")S8W)I
M<'1S(CH@>PH@(" @(G1E<W0B.B B96-H;R!<(D5R<F]R.B!N;R!T97-T('-P
M96-I9FEE9%PB("8F(&5X:70@,2(*("!]+ H@(")K97EW;W)D<R(Z(%M=+ H@
M(")A=71H;W(B.B B(BP*(" B;&EC96YS92(Z("))4T,B+ H@(")D979$97!E
M;F1E;F-I97,B.B!["B @(" B=',M;F]D92(Z(")>,3 N-"XP(BP*(" @(")T
M>7!E<V-R:7!T(CH@(EXT+C4N-2(*("!]+ H@(")D97!E;F1E;F-I97,B.B![
M"B @(" B0'1Y<&5S+W5U96YC;V1E(CH@(EXP+C N,2(L"B @(" B=75E;F-O
49&4B.B B7C N,"XT(@H@('T*?0H 

Kids, back in the 1980's we had to deal with files like this to ship a file from one place to another. There was a worldwide network of Unix machines running UUCP and cooperatively exchanging files from one computer to another. Those of us with grey beards earned our stripes with tools like this.

But we're here to talk about the present. What we just showed is using ts-node to run TypeScript on Node.js. The next thing we need to learn about is compiling, which is done as so:

$ npx tsc encode.ts --target esnext \
    --moduleResolution node \
    --module commonjs

The --target option says to output generated JavaScript using the latest ES2021 (and later) format. The --moduleResolution option says to search for modules using the Node.js algorithm. The --module option says to generate code in the CommonJS format.

As of this writing, the above generates this output in encode.js:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const uuencode_1 = require("uuencode");
const fs_1 = require("fs");
async function encodeFile(fn) {
    const text = await fs_1.promises.readFile(fn);
    return (0, uuencode_1.encode)(text);
}
(async () => {
    console.log(await encodeFile(process.argv[2]));
})();

Squint real hard and you see the original program is there, just rewritten a bit. It took some experimentation to make sure the generated code was this small.

Then, of course, to run the result:

$ node encode.js package.json 

You'll again see that archaically encoded version of package.json.

We've successfully compiled a TypeScript application, and viewed the generated JavaScript.

Automating the compilation

The next step is to make sure this task is automated so we don't have to waste precious brain cells remembering how to run this fine piece of software.

We could take the command line above and add it as a script in the scripts section of package.json. But, there's something better we can do.

A more effective way to express the configuration is through the TypeScript configuration file, tsconfig.json. Create a file by that name, containing this:

{
    "compilerOptions": {
        "rootDir": "./src",
        "outDir": "./build",
        "lib": [ "es2021", "esnext" ],
        "target": "es2021",
        "module": "esnext",
        "esModuleInterop": true,
        "moduleResolution": "Node"
    }
}

This configuration works for modern Node.js versions.

The lib parameter includes definitions and support for the ES5/ES6/etc specifications, which is what Node.js 14.x implements. Modern Node.js versions are up-to-date with the latest ECMAScript committee developments.

The target parameter describes the code to generate. TypeScript is not directly executable by any JavaScript engine, and must therefore be compiled to JavaScript. This target choice, es2021, means generated code uses the latest constructs. You can use es2021 if you prefer. With this target value, async/await functions are compiled using async/await keywords, rather than using Generator functions and the yield keyword.

The module parameter describes the module system to be used for the project. For Node.js we can use commonjs for traditional CommonJS modules, or for ES6 modules we can use es2020 or es2022 or esnext. The TypeScript documentation suggests using commonjs but, really, we want to move into the future, and that means adopting the ES6 module format. In this case we're using esnext.

The esModuleInterop fixes a few issues with compatibility between CommonJS and ES6 modules.

The moduleResolution parameter describes how to find modules. In TypeScript, the ES6 module format is used, meaning we use the import statement to retrieve a module, and we define exports using the export statement. TypeScript supports several algorithms for finding modules, which we control with this parameter. Using the node12 value tells TypeScript to use an algorithm matching the normal Node.js module resolution process, including support for ES6 modules.

The rootDir parameter says our source files must be in the src directory. The outDir parameter says compiled files will land in the build directory. That means we must create the src and build directories like so:

$ mkdir build src
$ mv encode.ts src

We've moved our program to the src directory, and running tsc will automatically compile anything in that directory generating code into the build directory.

The next automation step is in package.json add this script:

"scripts": {
    "build": "npx tsc"
},

With no arguments like this, the tsc command uses the tsconfig file to determine what to compile, and the configuration options.

$ npm run build

> ts@1.0.0 build
> npx tsc

And, indeed, build/encode.js contains this:

import { encode } from 'uuencode';
import { promises as fsp } from 'fs';
async function encodeFile(fn) {
    const text = await fsp.readFile(fn);
    return encode(text);
}
(async () => {
    console.log(await encodeFile(process.argv[2]));
})();

This is even cleaner than the previous generated code, because of the ES6 module format.

But, what happens if we run the file?

$ node build/encode.js package.json
(node:65920) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
...

The error here is that we have generated a file with the .js extension, but containing ES6 code. By default, Node.js treats files ending with the .js extension as CommonJS modules. But we have a file with ES6 code instead, which is supposed to have the .mjs extension.

It is not possible to get TypeScript to generate files with the .mjs extension. But, it is possible to convince Node.js to execute .js files using ES6 module format. To do so, in package.json we can add the following declaration:

{
    ...
    "type": "module",
    ...
}

This switches Node.js to presume .js files are in ES6 format. To use CJS format files, they now need to use the .cjs extension. Once we do this, the above command successfully encodes the file.

An alternate solution is in tsconfig change the module parameter to commonjs. This causes TypeScript to generate CommonJS modules rather than ESM.

Getting a taste for type checking in TypeScript

We came here for type checking, but we haven't seen any. Let's try a trivial example, which would be to feed a bad parameter to a function.

Add this to src/encode.ts:

async function unlinkFile(fn: string) {
    await fsp.unlink(123);
}

Notice that the parameter, fn: string, has a type specifier. That is, it only makes sense for an unlink function to take a string parameter, and that's what we've declared here. The fs module function unlink also takes a string parameter. Because we've installed @types/node we have all the type definitions for the Node.js platform.

Take a careful look at the code and see if you can spot the error. How many times have you accidentally written JavaScript that passed the wrong argument type to a function? Now, recompile the code:

$ npm run build

> ts@1.0.0 build
> npx tsc

src/encode.ts:16:22 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'PathLike'.

16     await fsp.unlink(123);
                        ~~~

Found 1 error.

THIS is what we came to TypeScript for. This sort of error is very common, and TypeScript just caught it for us. We didn't have to write a test case for this, we simply got notified right away. Any programmer seeing that message knows what to do, which is to feed the correct sort of data to that function call.

Additionally, if you've properly integrated TypeScript with your IDE, the error message will immediately be in front of you with no need to recompile.

IDE Support for TypeScript

Several IDE's have support for TypeScript.

Node.js type definitions for TypeScript

A very cool and most interesting feature of the TypeScript environment is the type definitions available both baked into the TypeScript runtime, and additional definitions available through the Definitely Typed project. ( (definitelytyped.org) http://definitelytyped.org/)

Unfortunately the Definitely Typed website does a poor job of explaining what it offers. This is a collection of type libraries for various tools and frameworks in the JavaScript ecosystem. These type libraries make it easier to use TypeScript with one of those tools or frameworks.

What that means is the type library for a given module helps the TypeScript compiler to know objects and functions and function parameters and more for that module.

Typescript includes a capability to implement a Declaration File whose purpose is to declare things. That’s what the Definitely Typed project does, is create well specified declaration files.

Adding the Definitely Typed definitions for Node.js brings in support for certain Node.js features.

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

This brings in a number of Node.js useful definitions. You don't declare anything in your code. Instead the TypeScript compiler tools automatically pick up the type definitions you've imported.

The Definitely Typed project has a large library of type definitions, and the website has a little search engine to look up what's available. Just don't expect any documentation to help you know what's what.

Generating declaration files for JavaScript generated by TypeScript

Declaration files inform the TypeScript compiler about the types used in a JavaScript module. Otherwise the TypeScript compiler would be unable to offer much help when writing code against a JavaScript module. Therefore it is best for all JavaScript module authors to bundle TypeScript declaration files.

This applies to cases where we generate a JavaScript module from TypeScript source. Making the resulting module useful to TypeScript requires declaration files.

Unfortunately the declaration files documentation does not provide much help for a newcomer looking to create these files.

Fortunately the TypeScript compiler can generate declaration files.

In tsconfig.json add these declarations:

{
    "compilerOptions": {
        ...
        "declaration": true,
        "declarationMap": true,
        "inlineSourceMap": true,
        "inlineSources": true,
        ...
    }
}

Technically all that's required is the declaration setting, and the TypeScript compiler will generate the .d.ts file alongside the .js file.

The other settings add Source Map files which are a kind of index into the source files used in generating the JavaScript code. With an inline source map the source map data is presented as a comment at the tail end of the file.

What did we learn about TypeScript?

The TypeScript compiler tools run on the Node.js platform, and install just like any other Node.js package. Hence it is very easy to use TypeScript as part of a Node.js project.

The Definitely Typed project is a companion project to TypeScript. The TypeScript language supports what are called declaration files that inform the TypeScript about the types used in a JavaScript module. The TypeScript compiler can generate the declaration files for you, as we just learned, or you can implement your own.

The Definitely Typed project is a collection of well-specified declaration files for JavaScript projects that do not implement their own.

About the Author(s)

(davidherron.com) David Herron : David Herron is a writer and software engineer focusing on the wise use of technology. He is especially interested in clean energy technologies like solar power, wind power, and electric cars. David worked for nearly 30 years in Silicon Valley on software ranging from electronic mail systems, to video streaming, to the Java programming language, and has published several books on Node.js programming and electric vehicles.

Books by David Herron

(Sponsored)