How Angular is reinventing itself with version 2 and TypeScript

AngularJS began to gain serious traction in 2012, attracting significant popularity as it cut through the noise in the crowded frontend framework landscape. Angular enthusiasm was quick to wane, however, as the framework fell victim to the trough of disillusionment that sits on the other side of the hype cycle. React is the new darling, perched on the peak of inflated expectations where it will likely be displaced by some shiny thing that comes along in the future.

The original AngularJS offered some good ideas, but it wasn’t a very good framework in practice. Its successor, Angular 2, addresses many shortcomings while retaining many of the strengths that made the original framework popular. With the benefit of a clean break, the developers behind Angular 2 were able to exorcise many of the framework’s most pernicious demons.

Users can finally put to rest the shambling horde of gratuitous $scope.$apply invocations that seemingly haunt every dark corner of AngularJS apps. And you can put a sharpened stake through the arcane parameter scraping hacks in the dependency injection system that used to bite during minification and other post-processing operations.

In short, almost everything is less awful. You’re still stuck with much of the distressingly esoteric terminology that the Angular developers afflicted upon the world, but I think many are willing to live with the lingering horror of ugly phrases like “directive transclusion” as long as nobody will never again have to degrade themselves by typing $scope.$apply.

Existing Angular enthusiasts will find that the things they know and love still endure in the framework’s new iteration. It is highly conducive to automated testing, you can compose your application with custom directives, and the data binding system supports one-way and two-way bindings against conventional JavaScript objects.

Angular 2 is still under heavy development, but I recently decided to take an early look. I used the latest Angular 2 beta to build a simple realtime chat application, paired with a backend built on Node.js and RethinkDB. I wrote the demo application in TypeScript, a language that pairs JavaScript with a flexible type system. I used TypeScript for both the Node.js backend and the Angular 2 frontend.

TypeScript

TypeScript allows the developer to annotate variables and function parameters with type declarations, which external tools can use to support features like static type checking and autocompletion. It is designed as a superset of JavaScript, adding new features without compromising the expected behavior of the original language. It supports a wide range of ES6 features, including the class keyword and arrow functions. Like CoffeeScript, TypeScript code transpiles to browser-friendly JavaScript, which means that you can use it to build frontend web applications.

TypeScript support is optional in Angular 2, but adopters will quickly find that it’s a great match for the framework. The Angular developers built the new version of the framework to take advantage of TypeScript’s strengths. They even collaborated with TypeScript’s core team at Microsoft to foster new language features that address specific Angular needs.

When building an Angular 2 application with TypeScript, you define each new directive in its own class. TypeScript decorators provide a convenient way to specify component metadata and properties, including the custom tag name and the list of external directives that you want the dependency injection system to make available for use.

TypeScript’s approach to type checking is unobtrusive and conducive to incremental adoption. You can choose to apply type declarations wherever you want, and leave them out when you don’t need them. You can also easily mix conventional ES6 code and TypeScript code in the same project without worrying about the boundaries or how the type system will affect interoperability.

In addition to simplifying Angular component definitions, TypeScript can offer a measure of additional safety and convenience throughout your project. When you work with arbitrary JSON data from a NoSQL database or a REST API, for example, you can use a TypeScript interface to formalize property access and avoid errors.

Consider a case where you are building a game and you have information about user scores. You could define the following TypeScript interface to describe the structure of the JSON objects that you store in your database:

export interface UserRecord {
  id: string;
  username: string;
  points: number;
}

When you fetch the associated records from your database, you can use a simple Array<UserRecord> type declaration to indicate that you are retrieving an array of objects that conform with the interface. In the following example, written in Node.js on the backend, I retrieve the documents from RethinkDB and iterate over them:

var r = require("rethinkdb");

(async function() {
  try {
    var conn = await r.connect({host: HOST});
    var users : Array<UserRecord> = await r.table("points")
                                            .orderBy(r.desc("points"))
                                            .limit(10).run(conn);
    conn.close();

    for (var user of users)
      console.log(`User ${user.username} has ${user.points} points`);
  }
  catch (err) {
    console.log("Failed to retreive user records from database.");
  }
})();

If you write code that accesses an undefined property, the TypeScript compiler will save you from shooting yourself in the foot. It will also intelligently offer autocompletion whenever you start accessing a property on a record in the users array.

In the code above, you will also notice that I was able to use ES7 async and await keywords, which TypeScript supports experimentally. You can use async and await to flatten and simplify code that expresses the flow of asynchronous operations. Out of the box, you can await any operation that returns a promise. Another major advantage of using await instead of .then is that it works seamlessly with conventional JavaScript exception handling.

You can share interface definitions, like the UserRecord example above, between frontend and backend code–using it on both sides of the stack. If you use a REST API or WebSockets to pass the underlying JSON objects to the frontend, you still get the same property access checks and basic type safety anywhere you use a type assertion to apply the interface.

It’s important to keep in mind that interface definitions and other features of the TypeScript type system only provide compile-time checking. In the case of the UserRecord definition, for example, it doesn’t perform any checks at runtime to ensure that the objects you receive actually match the structure that you define.

To get the full advantages of TypeScript, you will likely want to use a development environment that supports TypeScript integration. I built my demo application in GitHub’s open source Atom editor, with the atom-typescript plugin. It automatically compiles your TypeScript code into JavaScript when you save a file, exposing any errors that it finds along the way. It also has highlighting, autocompletion, symbol browsing, rename refactoring, and a number of other features.

Other editors that support TypeScript development include Visual Studio Code and WebStorm. You can also use command-line tools, though that doesn’t provide quite the same degree of workflow integration.

Angular 2

Angular 2 applications are built with components, discrete units of functionality that developers compose to achieve the desired result. Each component is an object that includes template markup, metadata, and supporting functions. When you build an Angular 2 application with Typescript, each component is a class definition. With the help of a special Component decorator, you can annotate the class definition with relevant component metadata like the component’s tag name, exposed properties, and emitted events. The following code example, a complete component from my chat demo, displays a single message sent by a user:

import {Component, View} from "angular2/core";
import {ChatMessageRecord} from "../interfaces";

@Component({
  selector: "chat-message",
  properties: ["message: message"]
})
@View({
  template: `
  <div class="message">
    <div class="sender"></div>
    <div class="time">&nbsp;()</div>
    <div class="text"></div>
  </div>
  `,
})
export class ChatMessage {
  message: ChatMessageRecord;

  get time() {
    return new Date(this.message.time);
  }
}

As you can see, the entire component is defined with a class called ChatMessage. The selector field in the @Component decorator indicates that chat-message is the name of the tag, which you can use in template markup when you want to create an instance of the component. The properties field tells the component how to expose class properties as tag attributes in template markup where the component is consumed. When you use the ChatMessage component, it will look a bit like this:

<chat-message [message]="someMessage"></chat-message>

You use the selector name to create the component instance, and then you can use data binding to attach the value of a variable to the message attribute. The square brackets around the attribute are used to signify that the data binding is one-directional instead of a two-way binding.

The @View decorator typically contains the component’s template and template metadata. In this particular case, I used a multiline string to embed the template markup directly. When the markup is more complicated, however, you would typically just provide the path of an external HTML file.

The @View decorator is also used for dependency injection. Like its predecessor, Angular 2 uses dependency injection to make a given unit of functionality available in a particular context. When a component uses other components in its markup, you add directives property to the @View decorator with an array of the components that you want to inject.

In my chat demo’s top-level application component, for example, I use three of my own custom components in addition to several from Angular’s standard libraries. In the following source code, you can see how I use the directives property to inject the imported components:

import {Component, View} from "angular2/core";
import {NgFor, NgSwitch, NgSwitchWhen} from "angular2/common";

import {ChatMessage} from "./components/message";
import {ChatLogin} from "./components/login";
import {ChatInput} from "./components/input";

import {ChatMessageRecord} from "./interfaces";

@Component({selector: "chat-app"})
@View({
  template: `
  <div class="inner" [ngSwitch]="username == undefined">
    <template [ngSwitchWhen]="true">
      <chat-login (login)="onLogin($event)"></chat-login>
    </template>
    <template [ngSwitchWhen]="false">
      <div class="messages">
        <chat-message *ngFor="#m of messages" [message]="m"></chat-message>
      </div>
      <chat-input (message)="onMessage($event)"></chat-input>
    </template>
  </div>
  `,
  directives: [
    NgSwitch, NgSwitchWhen, NgFor,
    ChatMessage, ChatInput, ChatLogin]
})
export class ChatApp {
  username: string
  messages: Array<ChatMessageRecord>

  onMessage(message: string) {
    fetch("/api/messages/create", {method: "post",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify({user: this.username, text: message})
    });
  }

  async onLogin(username: string) {
    this.username = username;
    this.messages = await (await fetch("/api/messages")).json();
    io.connect().on("message", message => this.messages.push(message));
  }
}

The NgSwitch component lets me conditionally choose between showing the login form or a list of messages depending on whether the user has logged in. The NgFor component lets me display a series of messages bound to an array. You can see that I’m using it on my own custom ChatMessage component, which encapsulates the logic for displaying a message.

When the user logs in, the application use the HTML5 fetch API to retrieve the initial messages. It also connects to Socket.io so that it can receive updates. When new messages arrive, it appends them to the array where the message list is bound. There’s also an onMessage event handler that fires when the user sends a message from the ChatInput component. It uses a POST request to push the user’s new message to the server.

Angular 2 is clearly a lot more intuitive than its predecessor. The example above is relatively straightforward with behavior that is easy to follow if you’ve ever worked with Angular 1.x or a similar frontend MVC framework.

There are a number of other noteworthy ways in which Angular 2 improves on its predecessor. For example, Angular 2 makes extensive use of RxJS observables. Under the hood, change detection and data binding are also more performant and flexible. It relies on Zone.js to obviate the need for conventional dirty checking and $scope.$apply.

How does it compare to React and Angular 1.x?

Angular 2 is radically different from its predecessor. Although the decision to pursue a clean break is understandably controversial, the result is a much stronger framework. It’s not really clear, however, if that’s going to be enough to attract and sustain a large audience. Now that the React hype cycle has displaced Angular’s, it’s going to be hard for Angular 2 to overcome that loss of momentum.

The Angular 2 component model and tight integration with Typescript are very impressive, but don’t help to differentiate it much from React. It’s worth noting that Typescript supports React’s JSX template language, which means that you can also comfortably use Typescript to build React components.

As a matter of taste, some developers might prefer Angular’s clean, declarative approach to template markup over React’s JSX. Another key difference is that Angular aims to provide a more comprehensive ecosystem whereas React largely focuses on view rendering. Many React adherents view React’s simplicity and tight focus as a selling point, however.

It’s not clear that Angular 2 will win back many of the React converts, but developers who liked Angular and left it behind to get away from the performance overhead of dirty checking and the poor ergonomics of $scope.$apply can happily return knowing that those warts are now vanquished.

For existing Angular 1.x users, Angular 2 offers clear benefits in performance and ease of development. Migrating existing applications is going to be a manageable undertaking: the framework includes an adapter that enables incremental upgrades, letting developers mix Angular 1.x and Angular 2.x components in the same application. For existing Angular 1.x shops, switching to 2.x for new projects will likely be beneficial.

Next steps

I’ve published the full source code of my chat demo on GitHub. You can check it out and try it yourself. If you’d like to learn more about RethinkDB, visit our ten-minute guide.

Resources: