TypeScript. First glance
Tags: typescript, beginners, programming, javascript
Published: 2022-01-17 20:45:24

## Why TypeScript?

A script is a sequence of commands, instructions in a programming language used to automate routine tasks. JavaScript was originally developed as a small scripting language that could allow you to perform simple operations on the browser side to improve the user experience. Its capabilities were very modest and were used for such purposes as: to show a notification to the user (alert), start a timer, make a running line or falling snowflakes. The main work of the site lay on the server side, including the generation of HTML markup.


Over time, more and more work began to be done on the browser side: form validation, creating stylized modal windows, carousels, slideshows, and so on. In order to simplify interaction with JavaScript and provide support between different browsers, various libraries such as **Prototype**, **MooTools** and **jQuery** began to appear.


So over time, more and more work began to be done on the client side, various frameworks appeared. In addition, JavaScript has been used to write the backend, CLI utilities, and even for mobile and desktop programs.


Despite the fact that JavaScript has increased in its capabilities, fundamentally little has changed in it. Thus, the level of language capabilities remained at the level of a simple scripting language, and the level of tasks that are solved on it has increased many times. It is extremely difficult to write and maintain modern, industrial applications in JavaScript.


Exactly for this reason, the TypeScript language was created. It is designed to **bring the missing features to JavaScript** and compensate for its shortcomings. At the same time, TypeScript is eventually compiled into JavaScript, which makes it possible to run it in any browser and in Node.js.


What exactly are the disadvantages of JavaScript in question and how TypeScript helps to solve them for clarity, we will consider examples. But to do this, we first need to make a minimal setup of the project.

## Create new project

Let's start a TypeScript project. Let's create a folder for it, for example **dev-to-project**, and initialize `packacke.json` in it. To do that, run the following commands in the console:

```bash
mkdir dev-to-project
cd dev-to-project
npm init
```

> If you don't have npm installed locally, check [this article](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) to find out how to do that

Now we need to install TypeScript in our project as a dependency. To do this, open the console and type:

```bash
npm install --save-dev typescript
```

This command will create a **node_modules** folder containing the installed typescript and **package-lock.json** file that captures versions of installed dependencies. In addition, a devDependencies section will be created in **package.json** file with the specified typescript version.


Now you can open the project folder in the code editor. In the root of the project, create an **src** folder with the **index.ts** file. And traditionaly, let's write some `Hello, world` in it:

```js
console.log('Hello World!')
```

Before going further, make sure that the structure of your project looks like this:

```
.
├── node_modules
│   ├── .bin
│   │   ├── tsc -> ../typescript/bin/tsc
│   │   └── tsserver -> ../typescript/bin/tsserver
│   └── typescript # bunch of sub-dirs inside
├── package-lock.json
├── package.json
├── src
│   └── index.ts
```

We cannot run TypeScript code without additional actions. We must first transpile any TypeScript code into JavaScript, and then run the already converted code.

To do this, you need to configure the build. Open the **package.json** file and change it as follows:

```json
{
  "name": "dev-to-project",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc src/index.ts --outDir dist --target es2015",
    "start": "node .",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Elijah Zobenko",
  "license": "ISC",
  "devDependencies": {
    "typescript": "^4.2.4"
  }
}
```

We have added the **build** command that uses the TypeScript compiler `tsc` along the path **node_modules/.bin/tsc**. The compiler will take our file **src/index.ts**, and the result will be sent by the `OutDir` directive to the **dist** folder, which will be created automatically. The `target` parameter sets the ECMAScript version to which our code will be converted.

We also created the **start** command, which launches the application. The `node .` construction will read the value of `main` from **package.json** and will launch the specified file.


Therefore, we changed the **main** option, specifying the path to the main executable file according to where it will be located after the build is executed - **dist/index.js**

Let's now assemble and run our code. To do this, run the following in the console:

```bash
npm run build
npm start
```

As a result, a message will appear in the console:

```
Hello World!
```

That should be enough for now. We'll come back to the settings later in this tutorial. In the meantime, let's get to know the TypeScript language directly.

## Meet TypeScript

Let's look at some basic TypeScript features that will immediately help demonstrate its best sides. In order to make the examples more visual, we will compare a similar piece of code written in JavaScript and TypeScript.


The code will contain a small set of books and a function that selects a suitable book for the user by genre and number of pages.

Let's create a temporary file **src/playground.js** and put the following code in it:

```javascript
class Book {
    constructor (name, genre, pageAmount) {
        this.name = name
        this.genre = genre
        this.pageAmount = pageAmount
    }
}

const books = [
    new Book('Harry Potter', 'fantasy', 980),
    new Book('The Fellowship of the Ring', 'fantasy', 1001),
    new Book('How to be productive', 'lifestyle', 500),
    new Book('A Song of Ice and Fire', 'fantasy', 999) 
]

function findSuitableBook (genre, pagesLimit) {
    return books.find((book) => {
        return book.genre === genre && book.pageAmount <= pagesLimit
    })
}
``` 

At first glance, there is nothing unusual in the code. On the one hand, it is so. Now let's try using the `findSuitableBook` function. To do this, add the following code:

```javascript
console.log(findSuitableBook('fantasy', 980))
console.log(findSuitableBook('fantasy', '1000'))
console.log(findSuitableBook('fantasy'))
console.log(findSuitableBook(1000, 'fantasy'))
console.log(findSuitableBook(1000))
console.log(findSuitableBook())
```

Despite the fact that the correct version of the call is only the first one, we do not receive any error messages. Besides the fact that we can swap arguments and pass a string instead of a number, we can not pass arguments at all. JavaScript does not react to this at all.

Let's run the following command in the console to look at all the call results:

```bash
node src/playground.js
```

Here's what we'll get:

```javascript
Book { name: 'Harry Potter', genre: 'fantasy', pageAmount: 980 }
Book { name: 'Harry Potter', genre: 'fantasy', pageAmount: 980 }
undefined
undefined
undefined
undefined
```

Despite the incorrectness of the second option, (`findSuitableBook('fantasy', '1000')`) it will work as needed because of converting types. During execution, the string `'1000'` will be converted to a number, since it is compared with another number - this is the internal JavaScript behavior. We can say that JavaScript has "fixed" the user's error. But how does JavaScript "fix” missing arguments? The missing arguments will be assigned the value `undefined`. The language itself will decide what the result should be when comparing a string with `undefined` and mathematically comparing a number with `undefined`.


A JavaScript developer may not notice problems in what is happening, as he is used to such behavior. However, at least two drawbacks can be noted - poor readability and non-obvious behavior of the code.

Poor readability lies in the fact that without reading the code, we will not be able to understand the types of arguments and which of them are mandatory and which are not. The `genre` argument could be a number equal to the `ID` of the genre. And if there is a condition in the code to check the `pagesLimit` parameter before using it, this would mean that the parameter can not be passed. Thus, when developing in JavaScript, you constantly have to reread the code before using it.


The non-obvious behavior of the code lies in the fact that the developer never knows exactly how the program will react, because it is simply impossible to know and take into account every detail of the internal structure of JavaScript. Non-obviousness leads to the concealment of problems that will sooner or later make themselves felt. And finding the cause and correcting it in such conditions is quite a difficult task.


Let's add another function call:

```javascript
console.log(findSuitableBook().name)
```

Up to this point, JavaScript solved all the problems by itself, hiding them from us and thereby depriving us of the opportunity to write high-quality code. Let's check what will happen now. Let's run the code execution as shown earlier.

Now we see an exceptional situation, the application has crashed with the following message:

```javascript
console.log(findSuitableBook().name)
                              ^

TypeError: Cannot read property 'name' of undefined
```

So, JavaScript couldn't figure out how to take a field from a non-existent value and decided to fall. Belatedly, we find out that there were problems in the code. Even in a small piece of code, we are faced with non-self-explanatory, non-obvious code that hides problems. Now let's look at what TypeScript has to offer.

Let's copy the code from **playground.js** in **index.ts**. You can immediately notice that some lines of code are underlined in red in the editor. TypeScript immediately found some problems in the code. Let's try to build a project and run it:

```bash
npm run build
```

Errors will appear in the console. Exactly the same ones that were underlined in the code editor. The code editor displays errors to improve the user experience. But the appearance of errors during assembly execution is a key point. Such a build ends with a non-zero status code. The developer not only sees the list of errors, but the process itself ends with an error. This is an important point because the build command is always executed during the deployment of the project. This behavior ensures that the code containing errors cannot physically be in production. At the same time, we have not yet used any feature of the TypeScript language.


Let's put the code in order so that the build is successful. Let's start with the `Book` class. From the JavaScript point of view, this section does not contain any problems. However, from the TypeScript point of view, the assignment of the properties `name`, `genre` and `pageAmount` cannot be performed because the properties are not declared in the class. We need to fix this. At the same time, we will immediately limit the types of values that these properties can take. The `name` and `genre` should be a string, and the `pageAmount` should be a number.

```ts
class Book {
    name: string
    genre: string
    pageAmount: number

    constructor (name: string, genre: string, pageAmount: number) {
        this.name = name
        this.genre = genre
        this.pageAmount = pageAmount
    }
}
```

Here we use TypeScript syntax for the first time. By putting a colon when declaring properties and constructor arguments, we specify their types. In this example, these are strings and numbers. We will get to know the type system more closely in the next lesson.

At this stage, the main thing to understand is that in this way we prevent the creation of all kinds of erroneous variations of creating a copy of the book. All the following lines of code contain errors and will not be skipped by the TypeScript compiler:

```ts
new Book(),
new Book('Harry Potter'),
new Book('Harry Potter', 'fantasy')
new Book('Harry Potter', 'fantasy', '980'),
new Book(980, 'Harry Potter', 'fantasy'),
```

And it's just wonderful! Every time a developer makes a mistake, he finds out about it instantly. At the same time, it receives information about the file, line, and even the essence of the problem. Such a problem can be easily and quickly localized.

We still have errors in the `findSuitableBook` function call block. It's easy enough to fix them. To begin with, we will delete all the lines that are marked as erroneous. Instead of seven lines, we will have only three. Here they are:

```ts
console.log(findSuitableBook('fantasy', 1000))
console.log(findSuitableBook('fantasy', '1000'))
console.log(findSuitableBook(1000, 'fantasy'))
```

It is necessary to write the function in such a way that it is obvious to the developer and compiler how it works. Now you can describe the action of the function as follows: the “find a suitable book" function accepts the "genre” and the page limit. That doesn't sound detailed enough. We need to do this: the “find a suitable book" function accepts the `genre` as a string and the `pageAmount` as a number, and should return the `Book`. Let's write it down like this:

```ts
function findSuitableBook (genre: string, pagesLimit: number): Book {
    return books.find((book) => {
        return book.genre === genre && book.pageAmount <= pagesLimit
    })
}
```

Now it is enough for the developer to read the first line of the function (its signature) to understand the meaning of what it does. The compiler easily cuts off the remaining incorrect options. Let's check that everything works as it should:

```bash
npm run build
npm start
```

The following should appear on the screen:

```javascript
Book { name: 'Harry Potter', genre: 'fantasy', pageAmount: 980 }
```

If you look at the file **dist/index.js**, then you can see that the code in it is one to one as it was in our **playground.js**. However, it has passed the transpilation stage from TypeScript, which means that it is secured. In addition, you will never have to work with it, because the work is done in the source __src/\*.ts__ files, and everything that is in **dist/\*.js** is only needed for execution.


It is worth noting that there is a system for JavaScript that tried to bring a similar experience to the language, namely, to bring transparency with respect to input arguments and return values of functions. The implementation of the system is a special syntax of JSDoc comments. This syntax is supported by many code editors. This is how special comments look for our `findSuitableBook` function:

```javascript
/**
 * @param {string} genre
 * @param {number} pagesLimit
 * @returns {Book}
 */
function findSuitableBook (genre, pagesLimit) {
    return books.find((book) => {
        return book.genre === genre && book.pageAmount <= pagesLimit
    })
}
```

However, this approach has limited effectiveness for several reasons. Firstly, the presence or absence of comments is entirely the responsibility and care of the developer. Secondly, non-compliance with the described signatures does not lead to errors, so problems in the code may continue to go unnoticed. Thirdly, such comments are not part of the language, so the code can be edited and the comments remain unchanged, which leads to even more confusion.


Let's talk a little bit about declaring the return value type. In the example above, in the file **index.ts** the result of the function execution is declared as `Book`. This helps in several cases. Firstly, readability improves, as we mentioned earlier. Secondly, it makes it impossible to return a value other than the specified one. For example, the following code will result in an error:

```ts
function findSuitableBook (genre: string, pagesLimit: number): Book {
    return {
        name: 'Harry Potter',
        // we just missed the `genre` property
        pageAmount: 980
    }
}
```

Now let's refactor the function so that it can return both one and several results. In this case, by default, the function will return multiple results.

```javascript
/**
 * @param {string} genre
 * @param {number} pagesLimit
 * @returns {Book}
 */
function findSuitableBook (genre, pagesLimit, multipleRecommendations = true) {
    const findAlgorithm = (book) => {
        return book.genre === genre && book.pageAmount <= pagesLimit
    }

    if (multipleRecommendations) {
        return books.filter(findAlgorithm)
    } else {
        return books.find(findAlgorithm)
    }
}

const recommendedBook = findSuitableBook('fantasy', 1000)
console.log(recommendedBook.name)
```

We added a new argument `multipleRecommendations`, which by default has the value `true`, changed the search algorithm and left only the correct function call. Here you can immediately notice several things. Since the default argument is `true`, this affects all existing code. At the same time, the new argument was lost in JSDoc, and the return value type remained the same - a common thing. Therefore, the code `console.log(recommended Book.name )` remained unchanged and will currently result in a request for the name field from the array. Again, non-obvious behavior with hiding problems in the code.

Let's make similar changes in the TypeScript code:

```ts
function findSuitableBook (
    genre: string,
    pagesLimit: number,
    multipleRecommendations = true
): Book {
    const findAlgorithm = (book: Book) => {
        return book.genre === genre && book.pageAmount <= pagesLimit
    }

    if (multipleRecommendations) {
        return books.filter(findAlgorithm)
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    } else {
        return books.find(findAlgorithm)
    }
}

const recommendedBook = findSuitableBook('fantasy', 1000)
console.log(recommendedBook.name)
```

In this case, we will get a compilation error due to the discrepancy between the described return value type and the real one. Let's fix it:

```ts
function findSuitableBook (
    genre: string,
    pagesLimit: number,
    multipleRecommendations = true
): Book | Book[] {
    const findAlgorithm = (book: Book) => {
        return book.genre === genre && book.pageAmount <= pagesLimit
    }

    if (multipleRecommendations) {
        return books.filter(findAlgorithm)
    } else {
        return books.find(findAlgorithm)
    }
}

const recommendedBook = findSuitableBook('fantasy', 1000)
console.log(recommendedBook.name)
                            ~~~~
```

We replaced `Book` with `Book | Book[]`, which means that either one book or an array of books will be returned. To which the compiler immediately reacted with another error. The fact is that before taking a name from a book, you need to make sure that it is not an array of books. Let's finalize the code as follows:

```ts
const recommendedBook = findSuitableBook('fantasy', 1000)

if (recommendedBook instanceof Book) {
    console.log(recommendedBook.name)
} else {
    console.log(recommendedBook[0].name)
}
```

The solution lies in adding an additional check. In this case, we checked whether the result is an instance of the Book class. As you can see, TypeScript will always find an error and tell you where to look for it. Let's make sure everything works correctly. The following should appear in the console:

```
Harry Potter
```

Great! You can delete the file **src/playground.ts**, we won't need it anymore.
So far, we have considered only a tiny part of the TypeScript features. With each article, we will learn more about the language, and in the next one we'll set up the project for convenient further work on it.