A TypeScript & alt JS grab bag
AltJS, an otherwise unrelated family of languages that compilei to JavaScript has been on my radar for a while now. I'm following three languages closely: ClojureScript (cljs), Roy, and TypeScript. Two primarily functional style languages (cljs, Roy), two with static types (Roy, TypeScript).
ClojureScript is a dynamic lisp that is a relative of the JVM language Clojure. It features an extensive standard library including a wide range of immutable datastructures. The Google closure optimizing compiler is used to ensure the compiled code does not include unused parts of the standard library. Were I not also a Clojure programmer, I would probably not be particularly interested in cljs.
Roy brings opinionated static typing to the table, in the ML tradition. Like ClojureScript it is functional, and defaults to immutability. There is basically no standard library to speak of at the moment, and the type system (one of its key features) is in flux. Of the three, Roy is the most interesting, but perhaps the least practical (currently anyway).
TypeScript brings Big OO in the style of C# (or Java) to JavaScript. Of the three it is the closest to JavaScript. The recently released version 0.9 includes proper parametric types (generics). Static types without generics is a barbaric regression, so this makes the language viable.
One common trait all these languages possess is straight forward interoperability with plain JavaScript.
Ideally I would prefer a functional language, and want strong support for that style of programming, but I also need a language that I can use in my work. While I have some leeway to choose languages and tools, the expectation is that a later contractor would still need to be able to maintain the code. In practise this rules out anything that deviates too far from the horrible “familiarity” test. This is the primary reason I am interested in TypeScript.
TypeScript is a superset of JavaScript syntax. Not only that, it brings future JavaScript syntax into use today: modules compiling to closures, classes compiling to prototypes, fat arrow lambdas. Any valid JavaScript is also valid TypeScript. The walkthrough on the TypeScript Playground is a good example of this.
A fluent JavaScript programmer can transition relatively easily into TypeScript, and they should have a good chance of guessing at what the generated code will look like.
Generally this is advantageous, but it is double edged; programs are going to tend towards mutability by default, written in an imperative or OO style. Writing functional style code is about as onerous as in JavaScript and there is still a lot of brace and parenthesis clutter.
An informal hierarchy of type systems:
- Stupid: Any barbaric system lacking at least one of parametric types or (local) type inference. Java (especially Java 4) is the poster child for this.
- Basic Correctness: Warns you when you are getting interfaces wrong, ensures you aren't smashing
strings
intoints
, etc. Generics are present here. Recent C♯ represents this well. - Expressivity: Allows you to express ideas that would be awkward or even impossible without the type system. Higher kinds make their appearence. Haskell is the obvious exemplar.
- Indistinguishable From Magic: Coq, Agda.
Of the languages I've been exploring, TypeScript is around level ii, Roy is aiming for level iii. ClojureScript with core.typed
would fall in level ii.
The style of programming required for level iii fails the “familiarity” test for most programmers, so at best I'm hoping to gain basic correctness. I am specifically looking for the type system to catch stupid errors in the short term, and ensuring that long term maintenance introduces as few new bugs as possible.
TypeScript's type system is almost bog standard OO style with the required bells and whistles required to type typical JavaScript usage and idioms. To this end everything is built on subtyping, and strongly driven by interfaces. There are two interesting features that are not so common in Big OO: structural types and gradual typing.
Structural typing simply allows the interfaces to be used to describe objects by their contents not their class. You could think of it as checked duck-types. For example:
interface Person {
name: string;
age: number;
}
class Programmer {
name: string;
age: number;
constructor(name, age) {
this.name = name;
this.age = age;
}
}
function alertPerson(p: Person) {
alert("name: " + p.name +", age: " + p.age);
}
alertPerson(new Programmer("Andrew", 30));
Note that alertPseron
's parameter has the type of Programmer
, not Person
, yet it is passed to alertPerson
(that requires a Person
) without error; the compiler infers that p is structurally an instance of Person
. Roy also implements structural typing to allow interop with JavaScript.
Gradual Typing is more interesting. This is present in TypeScript and core.typed
. Gradual Typing basically blurs the boundaries between typed and dynamic code. Untyped code is represented by the any
type.
I was surprised how well this works in practise. The only caveat seems that you can't have identifiers that TypeScript isn't aware of. For example:
function foo() {
return baz(); // tsc is unhappy with this.
}
foo
would be compiled by JavaScript, with the assumption that baz
is defined elsewhere. TypeScript on the other hand requires you to at least declared it. E.g.:
declare var baz; // baz is declared as any.
function foo() {
return baz(); // tsc is happy now
}
The gradual typing and structural types are nice tools that work well with JavaScript idioms. Unfortunately, the hammer in TypeScripts toolkit for defining the relationship between types is inheritance. In contrast core.typed
(and Typed Racket I believe) allow users to define relationships between types ad hoc (and alias them to convenient names if desired) by describing unions or intersections of other types. The union states that the value must be of one of the types in listed, while the intersection states that the value is an instance of all the types listed.
Inheritance brings with it type assertions. These are similar to casts, although I don't yet understand the exact differences. The most common area where I've needed to use assertions is dealing with DOM code. For example:
var div = document.createElement("div"); // returns is typed as HTMLDivElement
div.setAttribute("foo", "bar")
var cloned = div.cloneNode(true); // return is typed as Node
alert(cloned.getAttribute("foo")); // compiler complains
alert((<HTMLElement> cloned).getAttribute("foo")); // compiler is appeased
More problematic is variance. As of version 0.9, TypeScript does not appear to support any control over co-, contra- or invariance. For example:
class Animal {
constructor(public name) {}
toString() { return this.name; }
}
class Dog extends Animal {
fetch (thing:any) {
alert("fetching: " + thing);
}
}
class Cat extends Animal {
sleep (hours: number) {
alert("sleeping for " + hours + " hours");
}
}
var cats1:Cat[] = [new Cat("Eek"), new Cat("Horse"), new Cat("Mister")];
var cats2:Cat[] = cats1.slice(0);
function addAnimal(animals:Animal[], animal:Animal) {
animals.push(animal);
}
var rover:Dog = new Dog("Mouse");
addAnimal(cats1, rover);
alert(cats1.join(", ")); // there is a dog in the array of Cats!
cats2.push(rover); // Type error as expected
This is a large hole in the soundness of the type system. Hopefully the designers (who have added variance constraints to C♯) will fix this in a future version.
The utility of TypeScript is increased due to the presence of the Definitely Typed collection of type definitions for popular JavaScript libraries, and the comprehensive standard library of types for the browser objects that ships with the compiler.
The down side of this (necessary) approach is that type definitions can be out of date relative to the library they annotate, and may not be correct or comprehensive. A related issue is that when TypeScript 0.9 was released, many of the type definitions needed to be updated to include generics annotations.
Roy's goals aim higher than TypeScripts. The type system is intended to be sound, and more expressive. The down side is that it is less complete. Two improvements coming with the current type system overhaul are row polymorphism and higher kinded types.
Row polymorphism is an alternative to subtyping for implementing records / structural types, without some of the typing head aches.
Perhaps more interesting is higher kinded types. A higher kinded type allows the type system to refer to and reason about a generic type that has not yet had it's type variables instantiated. In other words, given a type Parametric<T>
, you could reference Parametric
alone in a type expression.
An example of the sort of expressiveness that higher kinds offer can be found in Brian McKenna's (also Roy's author) fantasy land spec for Javascript. The abstractions described in the spec can be (I believe) defined in TypeScript, but the real power, writing functions in terms of the abstractions but not the concrete implementation of that abstraction is not possible without higher kinds.
ClojureScript has heavy weight compiler insfrastructure that makes it more difficult to get started with, and to integrate into an existing workflow. It is specifically targeting large applications in the browser. In contrast, both Roy and TypeScript have relatively snappy compilers and can easily scale down to small scripts on otherwise ordinary web pages.
One advantage of ClojureScript is that it implements a comprehensive range of data structures such as vectors, hashmaps, sets etc. Not only that, they are immutable and leverage structural sharing to minimise memory use. The downside is that these structures are currently pushing the limits of what is reasonable in a javascript environment, and some runtimes are still sluggish.
At work we build sites with the Django framework, and use Web Assets to handle processing and minifying our script and stylesheet assets. The current stable version of Web Assets (v0.8) only supports older versions of the TypeScript compiler, so I have had to backport the filter to the current stable release to support version 0.9 of TypeScript.
This works pretty well, but it does make debugging a bit of a pain; with the relatively primitive assets definitions I am currently using does not emit source maps, and (due to concatenating all the typescript files together prior to sending them to tsc
) renders line numbers in errors a bit useless.
A minor annoyance is that tsc
is currently quite slow. Apparently this is going to change in an upcoming release, and is the result of making it a better suited to tooling (such as Visual Studio) integration.
- transpile, ugh.