Harmony with TypeScript
On the way to harmony
Not so long ago Microsoft announced a new language targeting front-end developers. Everybody’s first reaction was “Why on Earth?!” Is it just Microsoft taking a shot at Google? Is that a trick to promote Visual Studio?
But anyway, what’s wrong with JavaScript? Why is everybody so keen to replace it? For a language designed in just two weeks, JavaScript is freakishly powerful and flexible. It was released in December 1995 and standardized as ECMA-262 in June 1997. Since then it has been evolving, and ECMAScript 5 (JavaScript 1.8) is now implemented in all major browsers. But JavaScript still has its bad parts.
The ECMA committee decided to maintain backward compatibility and introduced a "use strict" directive to switch the interpreter to a mode where the worst JavaScript behaviors trigger errors. That helps, but it doesn’t fix the type-guessing problem. Dynamic typing is often cited as a benefit of JavaScript, but it’s also the source of many bugs. Consider this example: a function makes a decision based on the value of an argument. We take the value of element.style.opacity and pass it in. Any check that the argument equals 1 or 0 will fail, because style properties, even when they look like numbers, are strings. An error that’s not easy to spot.
We could avoid this by checking argument types at the function entry point. But that adds cross-cutting concerns (code unrelated to the main logic) into every function. Other dynamically typed languages have type hinting. Why is there nothing like that in JavaScript?
Back in 1997 the ECMA committee started working on a 4th edition, building heavily on Netscape JavaScript 2.0. ECMAScript 4 was to include optional type annotations, type hinting, classes, modules, generators, iterators, destructors, and algebraic data types. It didn’t go smoothly because the proposed changes would have broken backward compatibility with previous editions. The committee debated for 18 months and ultimately abandoned the edition. The ECMAScript 4 proposal was superseded by a new project code-named ECMAScript Harmony, which may end up being called ECMAScript 6th edition. ECMA is well aware of JavaScript’s flaws, but a new standard could take years.
Some engineers at Google decided that no amount of improvement could fix JavaScript. The only way to have a decent client-side language was to build one from scratch. So they presented their Dart language. Dart is a class-based, single-inheritance language with interfaces, abstract classes, modules, and optional typing. Doesn’t that sound familiar? Still, it offered developers a better language right now. Isn’t that a solution? Brendan Eich, creator of JavaScript and CTO at Mozilla, said: “Dart is one of the many languages that currently compiles to JavaScript, and that’s a lot to say about that because like Google’s Native Client, I don’t think Dart is going to be natively supported ever in other browsers. Not in Safari, not in IE.” Whatever Dart’s merits, no browser except Chrome had plans to embed a Dart VM. Dart compiles to JavaScript, but the output gets bloated with the Dart support library and is hard to debug. The community was large and optimistic, but it already looked like a dead end.
Now we have TypeScript, another language for front-end development. Will this story be any different?
Ladies and gentlemen, here is TypeScript
So what is TypeScript? It’s free and open source, available to clone or fork on CodePlex. Rather than introducing entirely new syntax like Dart or even CoffeeScript, it borrows from ECMA proposals. TypeScript is first and foremost a superset of JavaScript, as Nicholas Zakas puts it. You write regular JavaScript inside TypeScript and it stays valid. When done, you compile TypeScript to JavaScript. The default output is ECMAScript 3-compliant, but you can target ECMAScript 5 with a compiler option.
So it’s not about replacing JavaScript; it’s about augmenting it with ECMAScript Harmony features. Brendan Eich believes Harmony could make it into the 6th edition as early as next year. Browser vendors plan to implement it quickly in their latest versions. If you need to support older browsers, you won’t be able to switch to the new syntax directly. With TypeScript you can use Harmony features today and the compiled output will work on virtually any browser. There’s no redundant code after compilation. Type checking is performed by the compiler and doesn’t appear in the output. Classes, interfaces, arrow expressions, and similar constructs compile down to patterns you likely already know. But enough background. Let’s see what the language actually is.
The language
Type annotation and hinting
TypeScript provides type annotation and static type checking. An annotated type can be any, one of the primitive types (number, string, bool, null, undefined), an object type, or void.
var inx: number = 1,
text: string = "Lorem",
isReady: bool = false,
arr: string[],
obj: ObjInterface = factory.getObj(),
mixedVal: any;
Object types have these members:
- Properties, which define the expected object structure (property-value type pairs)
- Call signatures, which define parameter types and return type for function invocation
- Constructor signatures, which define parameter types and return type for
newoperator usage
var obj: { x: number, y: number },
fn: ( x: number, y?: any ) => number,
constr: new() => ObjInterface;
Take this function expression that accepts an array and a callback:
var treatItems = function( arr,
callback ) {
// do something
return arr;
};
How much code would it take to add type checking manually? With TypeScript, it’s just this:
var treatItems = function(
arr: string[],
callback: (value: string,
inx: number,
arr: string[]) => string[]) {
// do something
return arr;
};
Compile it and you get:
var treatItems = function( arr,
callback ) {
// do something
return arr;
};
Notice any difference from the original JavaScript? There isn’t one.
Classes
The class syntax will look familiar:
class Mammal
{
private nickname: string;
constructor( nickname: string = "Noname" ) {
this.nickname = nickname;
}
public getNickname():string {
return this.nickname;
}
}
class Cat extends Mammal
{
private family: string = "Felidae";
constructor( nickname: string ) {
super( nickname );
}
public getFamily():string {
return this.family;
}
}
Here’s the compiled output. When one class extends another, the compiler adds this helper:
var __extends = this.__extends || function (d, b) {
function __() { this.constructor = d; }
__.prototype = b.prototype;
d.prototype = new __();
}
Concise and expressive. And the classes:
var Mammal = (function () { ... })();
var Cat = (function (_super) {
__extends(Cat, _super);
function Cat(nickname) {
_super.call(this, nickname);
this.family = "Felidae";
}
Cat.prototype.getFamily = function () {
return this.family;
};
return Cat;
})(Mammal);
The generated JavaScript looks like something a human wrote. Debugging it is straightforward.
Interfaces
Declare an interface and it becomes available for type annotation and hinting:
interface Point {
x: number;
y: number;
}
function getDistance( pointA: Point, pointB: Point ) {
return Math.sqrt(
Math.pow( pointB.x - pointA.x, 2 ) +
Math.pow( pointB.y - pointA.y, 2 )
);
}
var result = getDistance(
{ x: - 2, y: - 3 }, { x: - 4, y: 4 })
There’s no class implements interface syntax in TypeScript at this stage. I suspect that’s because of JavaScript’s dynamic nature.
One interface can extend another:
interface Mover
{
move(): void;
}
interface Shaker
{
shake(): void;
}
interface MoverShaker extends Mover, Shaker
{
}
Modules
The ECMAScript 6 proposal defines a module as a unit of source contained in a module declaration or an externally-loaded file. We’re used to local modules via self-invoking anonymous functions. In TypeScript, the export keyword controls what’s accessible from outside.
module graphic
{
export class Point
{
x: number;
y: number;
constructor( x: number = 0, y: number = 0 )
{
};
}
}
var point = new graphic.Point( 10, 10 );
External modules are familiar to anyone using AMD-compatible libraries (like RequireJS). TypeScript doesn’t introduce a module loader; it relies on AMD or CommonJS (configurable via compiler options).
// File main.ts:
import log = module ( "log" );
log.message( "hello" );
// File log.js:
export function message(s: string) {
console.log(s);
}
Arrow expressions
TypeScript brings some syntactic sugar from ECMAScript 6 proposals. Functions can be declared the same way as in CoffeeScript or Ruby:
(x) => { return Math.sin(x); }
(x) => Math.sin(x);
x => { return Math.sin(x); }
x => Math.sin(x);
This is particularly useful in callbacks. Unlike a regular function expression, an arrow expression doesn’t have its own this context, so we can avoid the that = this workaround:
var messenger = {
message: "Hello World",
start: function() {
setTimeout( () =>
{ alert( this.message ); }, 3000 );
}
};
messenger.start();
Type assertions
For polymorphic function returns, we can explicitly assert a type:
class Shape { ... }
class Circle extends Shape { ... }
function createShape( kind: string ): Shape {
if ( kind === "circle" ) return new Circle(); ...
}
var circle = <Circle> createShape( "circle" );
Ambient declarations
Ambient declarations provide type information for entities that exist outside TypeScript and are included by external means, such as a referenced JavaScript library.
Here’s an extract of type information for jQuery:
interface JQuery
{
text(content: string);
}
interface JQueryStatic {
get(url: string, callback: (data: string) => any);
(query: string): JQuery;
}
declare var $: JQueryStatic;
Source files dependencies
Type information for a large library can be a lot of code (see type info for YUI3). Mixing it with application code would be a mess. TypeScript provides a reference notation for this. In the example below, all jQuery type information lives in jquery.d.ts. The application code has the reference, and if we violate any jQuery interface, the compiler will catch it.
/// <reference path="jquery.d.ts" ></reference>
module Parallax
{
export class ParallaxContainer
{
private content: HTMLElement;
constructor( scrollableContent: HTMLElement ) {
$( scrollableContent ).scroll(( event: JQueryEventObject ) => {
this.onContainerScroll( event );
});
}
private onContainerScroll( e: JQueryEventObject ) : void {
// do something
}
}
Environment setup
The easiest way to install the TypeScript compiler is via NodeJS package manager:
npm install -g typescript
Compile a TypeScript file:
tsc example.ts
Want ECMAScript 5-compliant output?
tsc --target ES5 example.ts
You can even run TypeScript compilation at run-time.
Once you’re writing TypeScript, the development flow changes. With plain JavaScript, refreshing the page is enough to see changes. Now you need a build step to compile TypeScript files. My preferred tool is Apache Ant. I normally use Ant to compress and concatenate CSS and JavaScript, run unit tests, and on some projects preprocess SASS. You describe your build as XML; it’s easy to read and covers most requirements. Details are in
Maintainable JavaScript by Nicholas C. Zakas. Here’s an example showing how to add a TypeScript compilation task to your build script:
<?xml version="1.0"?>
<!DOCTYPE project>
<project name="tsc" basedir="." default="build">
<target name="build">
<!-- Compile all .ts files -->
<apply executable="tsc" parallel="true">
<srcfile></srcfile>
<fileset dir="." includes="**/*.ts"></fileset>
</apply>
<!-- Lint all required CSS, JS files -->
<!-- Concatenate all required CSS, JS files -->
<!-- Compress built CSS, JS files -->
</target>
</project>
Then just run:
ant
Conclusion
As Douglas Crockford said, “Microsoft’s TypeScript may be the best of the many JavaScript front ends.” For now, it’s probably the best way to start using ECMAScript Harmony. That said, TypeScript’s future isn’t entirely certain. This is an early experimental release meant to give developers a preview of what’s to come. It doesn’t look as raw as Dart, but it’s still risky to use on production projects. TypeScript is based on ECMAScript 6 proposals, and the spec could change significantly before ECMAScript 6 is finalized. When that happens, Microsoft will have to decide whether to track the spec or stay compatible with their current implementation. That ambiguity makes it hard to plan around. But TypeScript is definitely worth keeping up with.