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:
mkdir dev-to-project
cd dev-to-project
npm init
If you don't have npm installed locally, check this article 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:
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:
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:
{
"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:
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:
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:
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:
node src/playground.js
Here's what we'll get:
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:
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:
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:
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.
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:
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:
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:
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:
npm run build
npm start
The following should appear on the screen:
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:
/**
* @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:
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.
/**
* @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:
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:
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:
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.
Top comments (0)