Writing your first Hello World program in TypeScript with ease

In this lesson, we are going to learn about the basic structure of a TypeScript program and understand a few concepts of the compilation process. Then we will see how we can run the compiled JavaScript program using node and ts-node.

In the previous lesson, we learn about the history of JavaScript and how TypeScript solves some of the problems linked with the JavaScript development in general. We also understood the process of compiling a TypeScript program to JavaScript using the built-in TypeScript compiler so that we can run it inside a browser or Node.

When you install TypeScript, you get the tsc command that invokes the TypeScript compiler. The TypeScript compiler is designed to process TypeScript program files (ending with .ts extension) and converting them to vanilla JavaScript files (ending with .js extension).

The tsc command takes the file paths of these TypeScript programs as command-line arguments as well as compiler-options to customize the compilation settings using command-line flags. The command-line API of TypeScript has been explained in detail in the Compiler Flags lesson.

We can provide this information from a configuration file tsconfig.json as well. When you execute the tsc command without any command-line arguments, the TypeScript compiler looks for the tsconfig.json in the current directory. The file structure of tsconfig.json and how TypeScript compiler searches for it is explained in detail in the Compilation lesson.

When the TypeScript compiler processes TypeScript program files (let’s call them source files) and checks for any type related issues, it emits JavaScript files in an output directory. The output directory can be controlled using command-line flags or the tsconfig.json but if no value for it is provided then the output directory is the current directory.

TypeScript compiler can emit JavaScript in one of the two mode. The first and the default mode is the source-to-destination mapping. In this mode, each source .ts file will have its corresponding compiled .js output file in the output directory.

In this mode, the output file has the same name as the original source file, only the extension is changed from .ts to .js. TypeScript also likes to maintain the folder structure, therefore if a source file path is a/b/c.ts, then the output file is emitted with the same folder structure inside the output directory such as /dist/a/b/c.js. This is quite useful if you want to maintain the same file structure of the project in the output.

The second mode is bundled. In the bundled mode, all source .ts files are compiled into a single .js file. In this mode, you can give any filename you want to the output .js bundle file. This is useful when you want to ship a project as a library that can be imported from a single .js file in the browser (using a <script> tag). For example, you import a single jQuery bundle file in the browser but its project structure is divided into multiple files.

These modes are controlled using the command-line flags or the the tsconfig.json file. We also discussed these modes in detail in the Compilation lesson. In this lesson, we are not going to mess with it and only observe the default behavior.


Hello World Program

A simple Hello World program in JavaScript would be to print Hello World! message to the console. In TypeScript, just like in JavaScript, it would be using the console.log function call with a "Hello World!" string literal.

Let’s create a simple project with the directory name hello-world and place a hello.ts file inside it. The .ts extension will help us and the TypeScript compiler understand that it is a TypeScript file.

/hello-world
└── hello.ts

The hello.ts program looks like above. It’s plain and simple JavaScript, nothing TypeScript specific at the moment. To run this program inside a browser (by importing it using a <script> tab) or in Node.js, we need to convert it to the .js file. So let’s use the tsc command and provide the hello.ts as a source file.

The TypeScript compiler accepted the hello.ts file and created a hello.js file right beside the source file. Now we can import this hello.js file in the browser or run directly inside Node as shown above.

Adding Type Support

Let’s modify the previous example and let’s try to mimic a little modular project structure. Let’s also focus on making the program safe by adding TypeScript features such as type annotations.

/hello-world
└── src/
   ├── lib/
   |  └── utils.ts
   └── program.ts

In the modified project structure, the src directory contains all the source (.ts) files. The utils.ts contains a sayHello function that the program.ts imports and executes. These program files look like below.

The :number part of result variable declaration is called the Type Annotation. The type annotation declares the ultimate data type of an entity such as a variable, an argument of the function, or the return value of a function. This type annotation syntax placed just behind the name of the entity in the entity declaration statement.

The number here in the type annotation is the built-in data type provided by the TypeScript that represents all the numbers. You can also create your custom types that may represent complex values such as an object of a specific shape or a function of a specific signature.

Once an entity is annotated with a type, its type can’t be modified. That means an entity can represent only a set of values once it is declared. Therefore the result variable in the above program can only contain number values during the lifetime of this program. You will see in a minute why the above program fails to compile simply because of this reason.

When we compile these program files using the tsc command, we could provide file paths of both the files or just the program.ts since it imports utils.ts therefore it is auto included in the compilation process. Focus on the type annotations in the program. The sayHello function accepts an argument of type string and returns a value of type string.

💡 We will learn more about type annotation, basic and abstract types and the type system in general in the upcoming lessons. We have used import statement in the program above. We are also going to discuss these in upcoming lessons.

This type information will be used by program.ts and we can already see a problem here. The result variable assignment statement shows an error. If you hover on it, you will be able to see the problem. But let’s try to compile the program and see what TypeScript compiler says about it.

Oops, looks like we made a mistake. The sayHello function returns a value of type string but the result variable is a type of number. We can save a number value as a string value. So we need to either change the type of result to string, to change the return type of sayHello function.

Now if we compile the program, TypeScript won’t complain about anything and we also do not see any errors in the IDE. When the TypeScript compiler generated the compiler .js output files, it places them right beside the source files, since it likes to maintain the original file paths.

/hello-world
└── src/
   ├── lib/
   |  ├── utils.js
   |  └── utils.ts
   ├── program.js
   └── program.ts

Generally, we do not like to pollute our working directory. This is obviously bad and we need to fix it. What we can do is to provide an output directory where these files should be emitted. We can do that by invoking the tsc command with the --outDir command-line flag with the directory path.

Now the output files are placed inside dist directory but the TypeScript held on to the original file structure of the source file which is a good thing. Let’s see how compiled JavaScript looks like and what I said it’s a good thing.

By default, TypeScript converts ES6 import statement into CommonJS require() calls by default so that the output code can be run inside Node. This however doesn’t work inside a browser since the browser doesn’t support the CommonJS module system. But you can change it using --module flag.

You can also see that the TypeScript compiler performed some operations on the source code. It converted ES6 template string (inside sayHello function) into normal string concatenation using the + operator.

This process is called downlevelling since it down-compiles the code from a higher language version to a lower language version. This is done by the TypeScript compiler since the default target is set to ES3 (JavaScript version) which do not support template strings. You can change the target to ES6 or above to bypass this process using --target flag.

💡 You can learn more about these flags from the Compiler Flags lesson.

If we want to run this project using Node, we just need to invoke the node command and provide the dist/program.js file since it already imports the ./lib/utils.js file (relative to itself) using the require() call. Now you can see why keeping the original file structure in the output was a good thing. Had the output file structure different, Node would not have been able to find the ./lib/utils.js file relative to the program.js file.

Using tsconfig.json configuration file

So far we talked about --target and --module command-line flags and used the --outDir flag to change the output directory. These flags configure the compilation settings of the TypeScript compiler.

Similarly, we provided the source files to the TypeScript command from the tsc command itself. When the compiler-options get larger, so does the tsc command and it could be overwhelming to handle such a big command.

To solve this issue, TypeScript provides specifying these options through a JSON configuration file mainly named tsconfig.json. This file should be placed in the root directory of the project. When we invoke the tsc command from this root directory, the TypeScript compiler uses this file to extract the compilation settings just like values from the command-line options/flags.

So let’s create the tsconfig.json in the hello-world project directory.

In the above tsconfig.json, we have specified the source files to include in the compilation using the files field. The uitls.ts file is redundant since it is already imported inside program.ts. The compilerOptions field contains the actual compilation settings.

You can provide a custom file path of this configuration file using the tsconfig.json using the --project or -p flag such as tsc --project tsconfig.prod.json. You can also override compilerOptions of the imported tsconfig.json file by using the command-line flags.

In the above example, even though the tsconfig.json says the output directory is dist but we have overridden it using the --outDir flag. This could be useful while performing automated tasks.

Working with ts-node

So far we have learned that in order to run a TypeScript program, you first need to compile it and then run the output JavaScript (.js) files. But sometimes you really do not care about the output .js files, you just need to run the program using Node. So having this extra compilation step kinda seems unnecessary, though it is mandatory.

To solve this issue, some brilliant people came together and made ts-node. It is an open-source command-line utility to run .ts directly on Node without having to manually compile these source files to .js file since ts-node does that internally with added optimizations. You can just run ts-node program.ts command and the process we went through manually in the above example is taken care of by the ts-node under the hood.

To install this tool, follow this official documentation on the GitHub. I would recommend you to install this tool globally so that you can access it using the command ts-node from anywhere on your system. Once the installation is done, let’s move to our project and execute the ts-node command.

As you can see from the above example, we just executed the ts-node command and provided the src/program.ts file. The ts-node tool uses the TypeScript compiler to first compile the program and then run the output .js file using the node command. So in a nutshell ts-node is just a combination of tsc and node command but with added improvements.

If you want to provide custom compiler-options to ts-node, then you should put the tsconfig.json file in the directory where the ts-node command is being invoked, perhaps in the root directory of the project.

Just like the tsc command, the ts-node command looks for the tsconfig.json file but only to extract compilerOptions. The files field is ignored (also the include and exclude fields) so that we can provide the executable source file path from the command-line itself.

💡 You can override this behavior by providing the --file flags with the ts-node command if some extra files are needed to be added in the compilation. If you do not want ts-node to use the tsconfig.json file, the --skip-project flag. You can provide the compilerOptions object directly from the command-line using the--compiler-options flag. Use this documentation for more info.

#typescript #introduction