Original address: vsavkin.com/writing-ang…
Angular is written in TypeScript. In this article, I will discuss why we made our decision. I’ll also share my experiences with TypeScript: how it affects the way I write and refactor my code.
TypeScript has great tools
TypeScript’s biggest selling point is tools. It provides advanced auto-completion, navigation, and refactoring. Having such tools is almost a requirement for large projects. Without them, the fear of changing code keeps the code base semi-read-only and makes large-scale refactoring dangerous and expensive.
TypeScript is not the only type of language that compiles to JavaScript. There are other languages with stronger type systems that in theory can provide absolutely powerful tools. But in practice most people don’t have anything other than a compiler. That’s because building rich development tools must be a clear goal from day one, and it’s for the TypeScript team. That’s why they built language services that can be type checked and automated by the editor they use. If you want to know why so many editors have good TypeScript support, the answer is language services.
The importance of intellisense and refactoring cues (e.g., rename variables) to the code writing process and refactoring process is undisputed. It’s hard to measure, but I think refactoring that used to take days now can be done in less than a day.
While TypeScript greatly improves the code editing experience, it makes developer setup more complex, especially when compared to placing ES5 scripts on a page. In addition, you can’t use tools that analyze JavaScript source code (such as JSHint), but there are usually plenty of alternatives.
TypeScript is a superset of JavaScript
Because TypeScript is a superset of JavaScript, you can migrate to JavaScript without a lot of rewriting. You can move it module by module.
Simply pick a module, rename the.js file to.ts, and add type comments step by step. After completing a module, select the next refactoring module. Once you’ve written the entire code base, you can start tweaking the compiler Settings to make them more stringent.
This process may take some time, but it’s not the biggest problem for Angular. The gradual migration process allows us to keep developing new features while fixing bugs during the transition.
TypeScript makes abstractions explicit
Good design is about well-designed interfaces. And it’s much easier to express the idea of an interface in a language that supports them.
For example, imagine an application selling books that can be purchased through registered users of the user interface or through an external system with some API.
As you can see, both classes act as buyers. Although important to the application, the buyer’s meaning is not clearly expressed in the code. There is no file named buyeraser.js. Therefore, someone might modify the code without knowing what it means.
12345 |
function processPurchase(purchaser, details){ } class User { } class ExternalSystem { } |
It is difficult to determine what objects can act as buyers and what methods that role has just by looking at the code. We don’t know, and we’re not going to get much help from our tools. We have to artificially infer this information, which is slow and error prone.
Now compare this to the version where we clearly define the purchaser interface.
12345 |
interface Purchaser {id: int; bankAccount: Account; } class User implements Purchaser {} class ExternalSystem implements Purchaser {} |
The typed version makes it clear that there is a buyer’s interface, and then there are two classes that implement that interface. So TypeScript interfaces allow us to define abstractions/protocols/roles.
It’s important to realize that TypeScript doesn’t force us to introduce additional abstractions. The buyer abstraction exists in the JavaScript version of the code, but is not clearly defined.
In statically typed languages, the boundaries between subsystems are defined using interfaces. Due to JavaScript’s lack of interfaces, boundaries are not expressed in pure JavaScript. Without clear boundaries, developers began to rely on concrete types rather than abstract interfaces, and the code was very well-coupled.
My experience developing Angular, both before and after the transition to TypeScript, solidified my thinking. Defining interfaces forces me to think about API boundaries, helps me define interfaces for subsystems, and exposes areas of coupling.
TypeScript makes code easier to read and understand
Yeah, I know it doesn’t seem intuitive. Let me try to illustrate what I mean with an example. Let’s look at this function jquery.ajax (). What information can we get from the signature?
1 |
jQuery.ajax(url, settings) |
The only thing we can be sure of is that the function takes two arguments. We can guess the types. Maybe the first is a string and the second is a configuration object. But this is just a guess, and we could be wrong. We don’t know what options go into setting objects (their names and types), or what this function returns.
There is no way to call this function without looking at the source code or documentation. Checking source code is not a good option – the point of features and classes is to be able to use them without knowing how they are implemented. In other words, we should rely on their interfaces, not their implementations. We can check the documentation, but it’s not the best developer experience – it takes more time and the documentation is often outdated.
So while it’s easy to read jquery.Ajax (URL, Settings), to really understand how to call this function, we need to read its source code or its documentation.
Now compare this to the type version
12345678910111213 |
ajax(url: string, settings? : JQueryAjaxSettings): JQueryXHR; interface JQueryAjaxSettings { async? : boolean; cache? : boolean; contentType? : any; headers? : { [key: string]: any; }; / /... } interface JQueryXHR { responseJSON? : any; / /... } |
It gives us more information.
- The first argument to this function is a string.
- The Settings parameter is optional. We can see all the options that can be passed to functions, not just their names, but also their types.
- This function returns a JQueryXHR object, and we can see its properties and functions.
Typed signatures are definitely longer than untyped signatures, but: String, JQueryAjaxSettings, and JQueryXHR are not messy. They are important documents that improve the understandability of your code. We can understand the code to a much greater extent without having to implement it in depth or read the documentation. My personal experience is that I can read typed code faster because types provide more context to understand the code. But if any readers can find research on how types affect code readability, please comment.
TypeScript types are optional compared to other languages compiled to JavaScript, and jquery. ajax(URL, Settings) is still valid TypeScript. So it’s not all on and off. TypeScript is more of an enhancement. If you find that code without type comments is trivial to read and understand, do not use them. Use them only if they add value.
Does TypeScript restrict expression?
Dynamically typed languages have poorer tools, but they are more resilient and expressive. I think working with TypeScript makes your code rigid, but less so than you might think. Let me tell you what I mean. Suppose I use ImmutableJS to define personal records.
123456789 |
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 do we determine the type of record? Let’s define a Person interface.
1 |
interface Person { name: string, age: number }; |
If we try to do the following:
123 |
function createPerson(name: string, age: number): Person { return new PersonRecord({name, age}); } |
The TypeScript compiler will warn because the compiler does not know that the PersonRecord and Person types are compatible. Some people with experience in functional programming will say, “TypeScript only has dependent types!” . But that’s not true. TypeScript’s type system isn’t state-of-the-art. But its goals are different. This is not proof that the program is 100% correct. It’s more about giving you more tips and enabling more powerful tools. So shortcuts can be used when the type system is not flexible enough. So we can convert the created record by doing this:
123 |
function createPerson(name: string, age: number): Person { return <any>new PersonRecord({name, age}); } |
Type example:
1234567891011 |
interface Person { name: string, age: number }; const PersonRecord = Record({name:null, age:null}); function createPerson(name: string, age: number): Person { return <any>new PersonRecord({name, age}); } const p = createPerson("Jim", 44); expect(p.name).toEqual("Jim"); |
This code works because the type system is structured. As long as the created object has the correct attributes -name and age, it works fine.
You need to embrace TypeScript shortcuts. Only then will you find it very enjoyable to use the language. For example, don’t try to add types to some snazzy metaprogramming code – chances are you won’t be able to express them statically. In this case, you can configure type checking to ignore them. In this case, your code doesn’t lose much of its expressive power, but it’s instrumental and analyzable.
This is similar to trying to get 100% unit test code coverage. While 95% is usually not that difficult, 100% can be challenging and can negatively impact the architecture of your application.
The optional type system also preserves the JavaScript development workflow. Most of your application’s code base may be “corrupted,” but you can still run it. TypeScript continues to generate JavaScript, even if the type checker prompts an error. This is very useful during development.
Why use TypeScript?
There are many options available to front-end developers today: ES5, ES6 (Babel), TypeScript, Dart, PureScript, Elm, etc. So why TypeScript?
Let’s start with ES5. Compared to TypeScript, ES5 does not require conversion. This will keep your build setup simple. You don’t need to add file monitors, convert code, and generate source maps. It will work.
ES6 requires a converter, so the build setup isn’t that different from TypeScript. But it is a standard, which means that every editor and build tool supports ES6 or will support it. This is a weak argument, and it used to be that most editors had excellent TypeScript support at this point.
Elm and PureScript are elegant languages with powerful type systems that offer more functionality than TypeScript. Code written in Elm and PureScript may be simpler than similar code in ES5.
There are pros and cons to each of these options, but I think TypeScript is a good choice, making it an excellent choice for most projects. TypeScript takes 95% of good statically typed languages and brings them into the JavaScript ecosystem. You can still write ES6: you can still use the same standard libraries, the same third-party libraries, the same idioms and many of the same tools (e.g., Chrome developer tools). It gives you a lot without forcing you out of the JavaScript ecosystem.