Statically typed JavaScript via Microsoft TypeScript, Facebook Flow and Google AtScript

[2014-10-28] atscript, esnext, dev, javascript, typedjs, facebook flow, typescript
(Ad, please don’t block)

Update 2014-11-18: Facebook Flow has been released as open source. Its website is flowtype.org. The site mentions plans for Flow’s future.

This blog post looks at three initiatives for adding static typing to JavaScript: Microsoft’s TypeScript, Facebook’s Flow and Google’s AtScript.

Typing  

Let’s first clarify some terminology related to typing (excerpted from “Speaking JavaScript”).

Static versus dynamic  

In the context of language semantics and type systems, static usually means “at compile time” or “without running a program,” while dynamic means “at runtime.”

Static typing versus dynamic typing  

In a statically typed language, variables, parameters, and members of objects (JavaScript calls them properties) have types that the compiler knows at compile time. The compiler can use that information to perform type checks and to optimize the compiled code.

Even in statically typed languages, a variable also has a dynamic type, the type of the variable’s value at a given point at runtime. The dynamic type can differ from the static type. For example (Java):

Object foo = "abc";

The static type of foo is Object; its dynamic type is String.

Normal JavaScript is dynamically typed; types of variables are generally not known at compile time.

Benefits of static typing  

Does JavaScript really need static typing? Aren’t unit tests enough? Static typing isn’t magic: it does add complexity and visual clutter. This is especially true if you need to manually specify a type for everything (like in most of Java). Thankfully, neither of the three variants of typed JavaScript mentioned in this post force you to do so: if you don’t explicitly type an entity, they try to infer its type, by how the entity is used. Three examples:

  • If a variable is initialized with a value then the variable’s type is probably the same as the value’s type.
  • A number being passed as an argument leads to the initial assumption that the corresponding formal parameter has the type number.
  • The multiplication operator being applied to a parameter means that the parameter’s type is probably number.

Static typing offers the following benefits:

  • You get more errors at compile time. Early errors are good. This is usually faster than running unit tests and tends to catch a different category of errors.
  • It helps IDEs with auto-completion. Years ago, I used GWT (Java, compiled to JavaScript on the front end). Its statically typed DOM made it easy to explore that API.
  • Type annotations are useful for documenting parts of an API. I occasionally mention types in my JSDoc comments. If that information helps a static type checker then that is a nice side effect.
  • Checking and documenting types helps large teams collaborate, because it gives you an additional way of specifying what you require from or provide for your collaborators.

One more advantage is political: static typing (complemented by classes and modules) makes JavaScript more palatable for programmers coming from static languages such as Java and C# (i.e., many enterprise programmers). There is a danger of those programmers missing some of the subtleties of JavaScript, because it looks too familiar. However, more people liking a language that is still very recognizably JavaScript is a win, in my opinion.

Microsoft TypeScript  

TypeScript [1] is a subset of ECMAScript 6 [2] plus optional static typing. There are several ECMAScript 6 (ES6) features it doesn’t support yet (e.g. let, destructuring, string templates, promises, iterators and for-of loops) and it still uses an older version of the ES6 module syntax [3]. Both divergences from ES6 will disappear by version 2.0 [4], meaning that TypeScript will be a strict superset of ES6 (give or take ES6 features that are difficult to compile to ES5, such as generators and proxies).

As mentioned, static typing is optional in TypeScript and supported via:

  • Type annotations: You can annotate parameters, function results and properties to declare their types.

  • Generics: TypeScript supports generic type variables, generic types, generic classes and generic constraints.

  • Interfaces: enable you to describe the structure of a value. Interfaces match structurally (“duck typing”). Therefore, you can introduce interfaces for “existing” values – externally and without changing how those values are created (no need to “implement” like in Java or C#).

  • Visibility modifiers for instance properties declared in classes. If a property is marked private, it can only be accessed from “within” a class, external accesses produce compiler errors.

Let’s look at examples.

Type annotations. In the following code, the parameters x and y and the function results are declared to have the type number. Thus, the compiler shows an error for the function call in the last line.

function add(x : number, y : number) : number {
    return x + y;
}
add('a', 'b'); // compiler error

TypeScript compiles the function to the following ECMAScript 5 code. That is, all type information is gone at runtime, the end result is normal JavaScript.

function add(x, y) {
    return x + y;
}

Interfaces. In the following code, we demand that objects passed via the parameter x must have the number-valued property length.

function getLength(x : { length: number }) {
    return x.length;
}
console.log(getLength('abcd')); // 4
console.log(getLength(['a', 'b', 'c'])); // 3

Visibility modifiers. In the following code, x and y are private properties of Point instances. They can be accessed by the method dist(), but not from outside.

class Point {
    constructor(private x, private y) {
    }
    dist() {
        return Math.sqrt(this.x*this.x + this.y*this.y);
    }
}
var p = new Point(3, 4);
console.log(p.x); // compiler error: Point.x is inaccessible
console.log(p.dist()); // 5

The class Point is translated to this ECMAScript 5 code.

function Point(x, y) {
    this.x = x;
    this.y = y;
}
Point.prototype.dist = function () {
    return Math.sqrt(this.x * this.x + this.y * this.y);
};

.d.ts files  

TypeScript allows you to provide static type information for existing (untyped) code via external files, which have the file name extension .d.ts. The website DefinitelyTyped provides such files for many libraries. For example: jQuery, Backbone.js, Esprima, Express, gulp and Jasmine. That means that working with those libraries becomes more convenient if an IDE supports TypeScript. IDEs that do so are: Visual Studio, WebStorm, Eclipse (via TypEcs) and others.

As an example (one of several in the TypeScript manual) let’s assume that this is how the animalFactory API is used:

animalFactory.create("dog");
animalFactory.create("giraffe", { name: "ronald" });
animalFactory.create("panda", { name: "bob", height: 400 });
// Invalid: name must be provided if options is given
animalFactory.create("cat", { height: 32 });

The .d.ts file for this API would be:

interface AnimalOptions {
    name: string;
    height?: number;
    weight?: number;
}
function create(name: string, animalOptions?: AnimalOptions): Animal;

Facebook Flow  

Flow [5] is a type checker for ECMAScript 6 that is based on flow analysis. As such, it only adds optional type annotations to the language and infers and checks types. It does not help with compiling ECMAScript 6 to ECMAScript 5. Flow is already in use at Facebook and will be open-sourced “later this year”.

React lets you use Flow’s annotations by removing them while compiling its JavaScript dialect to plain JavaScript. Quoting the React Blog:

And lastly, on the heels of announcing Flow at @Scale yesterday, we're adding the ability to strip TypeScript-like type annotations as part of the jsx transform. To use, simply use the --strip-types flag on the command line, or set stripTypes in the options object when calling the API.

The Flow type system has support for extensible objects, open methods, prototypes, tuples, enums, generics, nullable types, union types, intersection types and more. All of these features are motivated by staying true to JavaScript’s nature, by the desire to capture how current JavaScript code is (implicitly) typed. Nullable types help prevent type errors caused by accessing properties if a value is undefined or null. You explicity specify whether, for example, a given parameter can be null (or undefined) or not. In the former case, the compiler forces you to check for null every time you access the parameter. In the latter case, the compiler warns you if you pass null or a nullable value.

Why not TypeScript? Flow’s type annotation syntax being compatible with TypeScript’s makes you wonder why Facebook doesn’t use TypeScript. Avik Chaudhuri mentioned [5:1] the following reasons:

  • Flow currently scales better than TypeScript. That is, it is faster for large programs.
  • Flow can infer more types, which means that it is more useful as a consistency checker for existing (completely unannotated) code bases.
  • Flow’s type system is richer. For example, TypeScript does not have non-nullable types.
  • Controlling the type checker enables Facebook to support their own technologies (e.g. React and its custom JSX syntax).

In order to scale, Flow runs as a server in the background and keeps itself up to date. Tools query the server. Flow’s type analysis is incremental, meaning that it can check modules in isolation. It supports many ECMAScript 6 features such as arrows, destructuring, optional parameters, promises, etc. Facebook is committed to track JavaScript standards as they evolve.

Google AtScript  

The preferred way to code AngularJS 2.0 will be AtScript [6]. It is compiled to ECMAScript 5 (for now) and all of the AngularJS 2 features will be accessible from ES5 code.

AtScript is ECMAScript 6 plus the following extensions:

  • Type annotations for variables, parameters and properties (with TypeScript-compatible syntax).
  • Meta-data annotations (which are called “annotations” in Java and “decorators” in Python).

In contrast to TypeScript and Flow, both data is available at runtime.

Runtime type checks  

You can turn type annotations into runtime type checks [6:1]: The following is AtScript code.

class MyClass {
    methodA(name : string) : int {
        var length : int = name.length;
        return length;
    }
}

The above code is equivalent to this ECMAScript 6 code:

import * as rtts from 'rtts';
class MyClass {
    methodA(name) {
        rtts.types(name, rtts.string);
        var length = tts.type(name.length, rtts.int);
        return rtts.returnType(length, rtts.int);
    }
}

The idea is to use runtime type checks during development, because they help catch errors when you are working with untyped code. For deployment, you wouldn’t insert them into the code.

Runtime type information  

Meta-data annotations mean that data is attached to annotated entities. For example, the following code uses two annotations, @Component and @Inject.

@Component({selector: 'foo'})
class MyComponent {
  @Inject()
  constructor(server:Server) {}
}

It is translated to:

class MyComponent {
  constructor(server) {}
}
MyComponent.parameters = [{is:Server}];
MyComponent.annotate = [
  new Component({selector: 'foo'}),
  new Inject()
];

AngularJS uses the runtime type information for dependency injection and to configure constructs such as directives. For example [7]:

@Directive({
  selector: ['[blink]']
})
class Blink {
  constructor(element:Element,
              options:Options,
              timeout:Timeout) {
    var selectors:Array<CssSelectors> =
        someThirdPartyAPI();
    element.query(selectors.join(','))
           .forEach(e => options.apply(e));
   }
}

Thus, while TypeScript and Flow throw type data away after compilation, AtScript keeps it around. This is the only way to make it available to runtime mechanisms such as dependency injection. It also enables you to type-check JSON you load at runtime.

Compiling to ECMAScript 5 and Dart  

What surprised me is that AtScript (.ats files) can be compiled to two target languages:

  • ECMAScript 5 (.js files), via Traceur [2:1], which supports AtScript’s language extensions
  • Dart (.dart files)

Given that AngularJS 2.0 is written completely in AtScript that means that there will be a single code base for both JavaScript and Dart.

A common standard?  

The teams of TypeScript, Flow and AtScript seem eager to collaborate. Avik Chaudhuri says so in his talk [5:2] and the TypeScript team mentions it on their blog [4:1]:

The TypeScript team is working with both the Flow and AtScript teams to help ensure that resources that have already been created by the JavaScript typing community can be used across these tools. [...] In the long term, we will also be working to fold the best features of these tools into ECMAScript, the standard behind JavaScript.

Furthermore, the following timeline is given [7:1] for AtScript’s features (without mentioning specific dates for each step):

  1. Runtime type checking: compile AtScript to .js and .dart
  2. Static type checking: IDE support
  3. Align with TypeScript
  4. ECMAScript proposal
  5. Browser support
  6. ECMAScript standard

Obviously, steps 5 and 6 are somewhat beyond the control of the AtScript team and depend on how things develop in the future.

Further reading  


  1. Welcome to TypeScript”, the official TypeScript homepage ↩︎

  2. Using ECMAScript 6 today ↩︎ ↩︎

  3. ECMAScript 6 modules: the final syntax ↩︎

  4. TypeScript and the Road to 2.0” by Jonathan Turner for the TypeScript Blog ↩︎ ↩︎

  5. Video: “JavaScript Testing and Static Type Systems at Scale” by Avik Chaudhuri and Jeff Morrison ↩︎ ↩︎ ↩︎

  6. AtScript Primer” by Miško Hevery ↩︎ ↩︎

  7. Slides: “Keynote: AtScript” from the ng-europe conference ↩︎ ↩︎