February 11, 2019











In this series, we go through the core concepts of Node.js. In general, in this series, we focus on the environment of Node.js and not the JavaScript itself, while having the advantages of static typing using TypeScript. It includes matters like the File System, Event Loop, and the Worker Threads. In this article, we create a script that can create and read files based on arguments passed when executing it. While we don’t create any particular web application here, we learn how Node.js handles files and server connections what might prove to be helpful in many situations. If you would like to know how to create applications with the Express framework, check out my Node.js TypeScript Express tutorial.

Node.js TypeScript: the basics of Node.js

At first, let’s elaborate on what Node.js is because it is sometimes misunderstood. In short, it is an environment that can run JavaScript outside of a browser, and that means that after installing Node.js, you can fire up your terminal and execute JavaScript code!

If you wrote some Front-End code, chances are you already have Node.js installed since Node Package Manager comes with it. You can find out with a simple command.

1 node - v

Modules

Although this series aims to use TypeScript, it is essential to understand how Node.js modules work and how to use them.

Let’s start by writing a quite peculiar piece of code:

1 2 3 console . log ( 'Hello' ) ; return ; console . log ( 'world!' ) ;

This example might seem odd because the ECMAScript specification states the following:

An ECMAScript program is considered syntactically incorrect if it contains a return statement

that is not within a FunctionBody.

When you run it, you can see that no errors were displayed and the program logged Hello. This due to the fact, that in Node.js, each file is treated as a separate module. Under the hood, Node.js wraps them in a function like this:

1 2 3 function ( exports , require , module , __filename , __dirname ) { // code of the module }

Thanks to that, the top-level variables are scoped to the module and are not global throughout the whole project. The module object can be used to export values:

utilities.js

1 2 3 4 5 6 7 8 9 10 11 12 function add ( a , b ) { return a + b ; } function subtract ( a , b ) { return a - b ; } module . exports = { add , subtract , } ;

To access it from another file, we use the require function.

main.js

1 2 3 4 const { add , subtract } = require ( './utilities.js' ) ; console . log ( add ( 1 , 2 ) ) ; // 3 console . log ( subtract ( 2 , 1 ) ) ; // 1

This module system is an implementation of the CommonJS format.

Node.js invokes the function that wraps our module a way, that the “this” keyword references to the module.exports. We can easily prove that:

1 console . log ( this === module . exports ) ; // true

The above is often a cause of confusion because if you are running Node.js in a console, the “this” keyword references to the global object.

As of the time of writing this article, the ES6 modules are under active development and might at some point be stable.

Global object

Being a JavaScript developer, you encounter the window object. In Node.js, we have the “global” object. You can think of it as its counterpart.

When your run Node.js in the terminal and executing a particular file, Node.js does not wrap your code in a module. When using Node.js in the terminal, you are in the global scope, the “this” keyword references to the global object. Then, the variables declared with var are attached to the global object:

This object is shared between all your modules. If we were to assign some property to it, it would be accessible everywhere.

Process arguments

The process object is a property of the global object, and therefore it is available throughout your application. It comes in handy when gathering information about the environment of our Node.js app, such as the currently installed version of Node.js. We dive into it more along the way in this series, and today we focus on the process.argv property. It holds an array containing the command line arguments you pass when launching the Node.js process.

The first element is the same as the process.execPath and it holds the absolute pathname of the executable that started the Node.js process.

The second element is a path to the executed JavaScript file. The rest of the process.argv elements are any additional command line arguments.

main.js

1 process . argv . forEach ( argument = > console . log ( argument ) ) ;

1 node . / main .js one two three

1 2 3 4 5 / usr / bin / node /home/m arcin / Documents /node-playground/m ain . js one two three

Running a Node.js Typescript project

To run our scripts using TypeScript we need to initialize our project using npm and install TypeScript and ts-node.

1 npm init - y

1 npm install - D ts - node typescript @ types / node

To run ts-node from within our node_modules directory, we make a new script in our package.json:

1 2 3 "scripts" : { "start" : "ts-node ./main.ts" }

For this series, I use a tsconfig file like that one:

tsconfig.json

1 2 3 4 5 6 7 8 9 10 11 { "compilerOptions" : { "sourceMap" : true , "target" : "es2017" , "alwaysStrict" : true , "noImplicitAny" : true , } , "exclude" : [ "node_modules" ] }

We’re all set to start using TypeScript with Node.js!

With TypeScript Node.js we use imports and exports like in ES6 Modules, but since Node.js does not yet support them, ts-node transpiles our code to CommonJS. You can find a full explanation of how ES6 Modules work in my other article: Webpack 4 course – part one. Entry, output, and ES6 modules.

File System

The fs module gives us an API to interact with the file system, for example, to read, create and delete files. All operations have synchronous and asynchronous forms, but it is heavily recommended to use asynchronous functions for better performance.

The asynchronous function always takes a callback as its last argument. Let’s create our first file using a TypeScript Node.js script.

writeFile

1 2 3 4 5 6 7 8 9 import * as fs from 'fs' ; fs . writeFile ( './newFile.txt' , null , ( error ) = > { if ( error ) { console . log ( error ) ; } else { console . log ( 'File created successfully' ) ; } } ) ;

To use the File System module in TypeScript, we first need to import it. Since it created with CommonJS style of exports, we can require the whole module with import * as fs.

The first argument of the writeFile function is the path of the file and the second one is the content of it.

Using callbacks might not be something you want here. With the help of a built-in utility called promisify, we can change the writeFile function in a way that it returns a promise.

1 2 3 4 5 6 7 8 9 10 11 12 import * as fs from 'fs' ; import * as util from 'util' const writeFile = util . promisify ( fs . writeFile ) ; writeFile ( './newFile.txt' , null ) . then ( ( ) = > { console . log ( 'File created successfully' ) ; } ) . catch ( error = > { console . log ( error ) ; } ) ;

To recreate some of the bash functionalities, let’s pass additional arguments to our script using process.argv. We start with an elementary version of the touch script that creates an empty file:

main.ts

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import touch from './utils/touch' ; const command = process . argv [ 2 ] ; const path = process . argv [ 3 ] ; if ( command && path ) { switch ( command ) { case 'touch' : { touch ( path ) ; break ; } default : { console . log ( 'Unknown command' ) ; } } } else { console . log ( 'Command missing' ) ; }

utils/touch.ts

1 2 3 4 5 6 7 8 9 10 11 12 import * as fs from 'fs' ; import * as util from 'util' const writeFile = util . promisify ( fs . writeFile ) ; export default function touch ( path : string ) { writeFile ( path , '' ) . then ( ( ) = > { console . log ( 'File created successfully' ) ; } ) . catch ( error = > console . log ( error ) ) ; }

To add additional arguments to an npm script, we need to prefix them with --.

1 npm start -- touch . / file . txt

Thanks to all the code above, the command creates an empty file.

readFile

The second function that we implement is “cat.” In our simple implementation, it can read a file.

utils/cat.ts

1 2 3 4 5 6 7 8 9 10 11 12 import * as fs from 'fs' ; import * as util from 'util' const readFile = util . promisify ( fs . readFile ) ; export default function cat ( path : string ) { readFile ( path , { encoding : 'utf8' } ) . then ( ( content ) = > { console . log ( content ) ; } ) . catch ( error = > console . log ( error ) ) ; }

1 npm start -- cat . / file . txt

The command above reads the file that is under that path. The second argument of the readFile method is an object with additional options. We use it to define the encoding of a file. Without it, the readFile function results with a Buffer.

The File System can do much more, and we cover features like streams and the file descriptors in the upcoming parts of the series.

Summary

In this article, we covered the very basics of TypeScript Node.js. It included how the modules work, what is the global object, the basics of the file system and how to pass additional arguments when running a script in Node.js TypeScript. There are many more topics to cover, so stay tuned!