Angular 2: why on TypeScript?

Original author: Victor Savkin
  • Transfer
Hello everyone who does not forget to look at our blog and during the traditionally non-working hours of the day!

Once upon a time in our publication of November 13, 2015, you convinced us to wait for the Angular 2 to be finished and publish a book about it. We are seriously going to take up such a project in the very near future, but for now we propose reading the detailed answer to the question posed in the title of this post.

Angular 2 is written in TypeScript. In this article I will explain why such a decision was made. In addition, I will share my own experience with TypeScript; what it is like to write and refactor code on it.

I like TypeScript, but maybe not for you.

Yes, Angular 2 is written in TypeScript, but applications in Angular 2 can be written without it. The framework works well with ES5, ES6 and Dart.

TypeScript is an excellent toolkit

The main advantage of TypeScript is its snap-in. It provides complex completion, navigation and refactoring. In large projects, one can hardly do without such tools . On your own, you never dare to permanently change the code, the entire code base is raw, and any major refactoring becomes risky and costly.

TypeScript is not the only typed language that compiles in JavaScript. There are other languages ​​with more stringent type systems that theoretically should provide absolutely phenomenal tools. But in practice, in most of them you will find almost nothing but the compiler. The fact is that the development of rich tools should be a priority goal from the very first day - this is exactly what the TypeScript team set itself . That's why language services were created here that can be used in editors for type checking and autocompletion. If you were wondering where so many editors with excellent TypeScript support come from, it's about the language services.

Predictive input (intellisense) and simple refactoring (for example, renaming a character) fundamentally change the writing process and, especially, code refactoring. Although this indicator is difficult to measure, it seems to me that refactoring, which used to take several days, is now done in a few hours.

Yes, TypeScript significantly optimizes code editing, but preparing for development with it is more difficult than, say, grabbing an ES5 script from a page. In addition, you lose the JavaScript tools for analyzing the source code (e.g. JSHint), but usually there are adequate replacements for them.

TypeScript is a superset of JavaScript

Since TypeScript is a superset of JavaScript, migrating to this language does not require a radical rewrite of the code. This can be done gradually, module by module.
Just take the module, rename the files .jsin it .ts, and then gradually add type annotations. Finished with the module - go to the next one. When the entire code base is typed, you can start messing with the compiler settings, making them stricter.

The whole process may take some time, but when we migrated Angular 2 to TypeScript, in the process we were able to not only develop new functions, but also fix bugs.

TypeScript abstractions become explicit

Good design means well-defined interfaces. And expressing the idea of ​​an interface is much simpler in a language that supports interfaces.

Suppose there is an application for buying books. Books in it are acquired in two ways: a registered user can do this through a graphical interface, and others through an external system that connects to the application through a certain API.



As you can see, the role of both classes in this case is the “buyer”. Despite the importance of the “buyer” role in this application, it is not explicitly expressed in the code. There is no file purchaser.js. Therefore, someone can change the code and not even notice that such a role exists.

function processPurchase(purchaser, details){ } 
class User { } 
class ExternalSystem { }

If you simply look at the code, it is difficult to say which objects can act as a buyer. Surely we do not know, and our tools will not especially help us with this. You have to fish out such information manually - and this is a slow business, fraught with errors.

And here is the version in which we explicitly define the interface Purchaser.

interface Purchaser {id: int; bankAccount: Account;} 
class User implements Purchaser {} 
class ExternalSystem implements Purchaser {}

In a typed version made clear that we have an interface Purchaser, and classes User, and ExternalSystemimplement it. So, TypeScript interfaces allow you to define abstractions / protocols / roles.

It is important to understand that TypeScript does not force us to add unnecessary abstractions . The Buyer abstraction is also in the JavaScript code, it just is clearly not defined there.

In a statically typed language, the boundaries between subsystems are defined using interfaces. Since there are no interfaces in JavaScript, drawing borders in pure JavaScript is more difficult. If the boundaries for the developer are not obvious, then it depends on specific types, and not on abstract interfaces, which provokes strong binding .

From the experience of working with Angular 2 before and after switching to TypeScript, this belief has only strengthened. When defining an interface, I am forced to think over the boundaries of the API; this helps me outline the public interfaces of the subsystems and immediately identify the binding if it happens by chance.

TypeScript makes it easier to read and understand the code.

Yes, I’m aware that at first glance it doesn’t seem like that. Then consider an example. Take a function jQuery.ajax(). What information is clear from its signature?

All that can be said for sure is that this function takes two arguments. Types you can try to guess. Perhaps the string comes first, followed by the configuration object. But this is just a version, perhaps we are mistaken. We don’t imagine what options can be in the settings object (neither their names nor types), we don’t know what this function returns.

It is not known how to call this function; one must consult the source code or the documentation. Checking the source code is not the best option; what is the use of functions and classes, if it is not clear how they are implemented. In other words, you need to rely on their interfaces, and not on the implementation. You can check the documentation, but the developers will confirm that this is an ungrateful work - time is wasted on the verification, and the documentation itself is often already irrelevant.

So, it’s jQuery.ajax(url, settings)easy to read , but to understand how to call this function, you need to read through either its implementation or the documentation.

Now compare with the typed version.

ajax(url: string, settings?: JQueryAjaxSettings): JQueryXHR; 
interface JQueryAjaxSettings { 
  async?: boolean; 
  cache?: boolean; 
  contentType?: any; 
  headers?: { [key: string]: any; }; 
  //... 
} 
interface JQueryXHR { 
  responseJSON?: any; //... 
}

This version is much more informative:

  • The first argument to this function is a string.
  • The argument is settingsoptional. We see all the parameters that can be passed to the function - not only their names, but also types.
  • A function returns an object JQueryXHR, we see its properties and functions.

A typed signature is definitely longer than an untyped signature, but :string, :JQueryAjaxSettingsand JQueryXHRis not garbage. This is an important “documentation” that makes code easier to read. You can understand the code much deeper without going into the implementation or reading of documents. Personally, I read typed code faster because types are a context that helps me understand the code. But, if one of the readers finds a study on how types affect readability, please leave a link in the comment.

One of the important differences between TypeScript and many other languages ​​compiled in JavaScript is that type annotations are optional, and jQuery.ajax (url, settings) is the most valid TypeScript. So, types in TypeScript can be compared rather than with a switch, but with an adjustment dial. If you believe that the code is trivial, and it can be read without type annotations, do not use them. Use types only when they are beneficial .

TypeScript limits expressiveness?

Tools in languages ​​with dynamic typing are so-so, but they are more plastic and expressive. I think with TypeScript your code will become slower, but to a much lesser extent than it might seem. I’ll explain now. Suppose I use ImmutableJS to define an entry Person.

const PersonRecord = Record({name:null, age:null}); 
function createPerson(name, age) { 
  return new PersonRecord({name, age}); 
} 
const p = createPerson("Jim", 44); 
expect(p.name).toEqual("Jim");

How to typify such a record? First, define an interface called Person:

interface Person { name: string, age: number };

If we try to do this:

function createPerson(name: string, age: number): Person { 
  return new PersonRecord({name, age}); 
}

then the TypeScript compiler swears. He does not know that PersonRecord is actually compatible with Person, since PersonRecord was created reflexively. Some readers familiar with FP may say, “Ah, if there were dependent types in TypeScript!” But they are not here. TypeScript is not the most advanced type system. But our goal is different: not to prove that the program is 100% correct, but to provide you with more detailed information and better tools. Therefore, it is quite possible to cut corners if the type system is not too flexible.

You can easily create the created record, like this:

function createPerson(name: string, age: number): Person { 
  return new PersonRecord({name, age}); 
}

Typed example:

interface Person { name: string, age: number }; 
const PersonRecord = Record({name:null, age:null}); 
function createPerson(name: string, age: number): Person { 
  return new PersonRecord({name, age}); 
} 
const p = createPerson("Jim", 44); 
expect(p.name).toEqual("Jim");

This works because the type system is structural. If the created object has the necessary fields - name and age - then everything is in order.

You need to get used to the fact that when working with TypeScript, “cutting corners” is normal. Only then will you be pleased to deal with this language. For example, do not try to add types to some fancy code for metaprogramming - most likely, you simply cannot express it statically. Conjure with this code and command the type checking system to ignore the fanciful part. In this case, you will almost not lose expressiveness, but the bulk of the code will remain convenient for processing and analysis.

The situation is reminiscent of an attempt to ensure absolute coverage with unit tests. 95% is usually done without problems, but achieving 100% is already a task, and such coverage can negatively affect the architecture of the entire application.

The optional type system also retains the familiar JavaScript development cycle . Large fragments of the code base may be “broken”, but you can still run them. TypeScript will still generate JavaScript, even if the type checking system remains unhappy. During development, this is extremely convenient.

Why TypeScript?

Today, front-end vendors have a wide selection of development tools: ES5, ES6 (Babel), TypeScript, Dart, PureScript, Elm, etc. Why TypeScript?

Let's start with ES5. ES5 has one major advantage over TypeScript: it does not require a transporter. Therefore, the entire development is easy to organize. You do not have to configure file watchers, translate code, or generate code cards. Everything just works.

In ES6, you need a transpiler, so the assembly itself will be organized approximately as in TypeScript. But this is a standard, meaning that all the editors or assembly tools will either support ES6 or will support it. Currently, TypeScript is already well supported in most editors.

Elm and PureScript are beautiful languages ​​with powerful type systems that can give your program much more than TypeScript. Elm and PureScript code can be much more concise than ES5.

Each of these options has its own advantages and disadvantages, but it seems to me that TypeScript is the golden mean, and is perfect for most projects. TypeScript has 95% of the benefits of good statically typed languages, and brings these benefits to the JavaScript ecosystem. The feeling is almost the same as if you were writing in ES6: you use the same standard library, the same third-party libraries, idioms and many familiar tools (for example, the "Development" section in Chrome). You get a lot of everything tasty, without leaving the familiar ecosystem of JavaScript.

Also popular now: