Concrete Types for TypeScript

A short tour of program evolution with StrongScript

We start from the following dynamically typed program:

1
2
3
4
5
var p:any = { x=3; z=4 } 
var f:any = func (p) {
  if (p.x < 10) return 10
  else return p.distance() }
f(p)
Without any loss of flexibility, programmers may choose to document their expectations about the argument of functions and data structures, and then annotate p and the argument of f with the optional type Point:

1
2
3
4
5
6
7
8
class Point {
  constructor(public x, public y){}
  dist(p) { return ... }
}
var p:Point = <any> { x=3; z=4 }  //Correct
var f:any = func (p:Point) { 
  if (p.x < 10) return 0
  else return p.distance(p) }     //Wrong
If the StrongScript compiler tsc is invoked on the example, we get:

$ tsc example.ts
example.ts(8,14): error TS2339: Property 'distance' does not exist on type 'Point'.
This cofirms that arbitrary objects can still flow into optionally typed variables (as at line 5), preserving flexibility (and ensuring trace-preservation), while the annotation of the argument of f enables local type checking, catching type errors such as the call to distance.

The programmer can also create instances of class Point, which are concretely typed as !Point, and pass them to f:

1
2
var s:!Point = new Point(5,6);
f(s);       // evaluates to 10
As function f has been type checked assuming that its argument is a Point, we known it's body will manipulate the argument as a Point. However, whenever an object which is an instance of a class is passed to an optionally or dynamically typed context, it protects its own abstractions at runtime.

Consider a new class definition, where the x and y fields have been strengthened as !number and as such can only refer to instances of class number:

1
2
3
4
5
6
class TypedPoint {
  constructor(public x:!number, public y:!number){}
  dist(p) { return ... :!number }}

  var t:!TypedPoint = new TypedPoint(1,2);
  (t).x = "o"    //DYNAMIC ERR: type mismatch
Some flexibility is lost by this class but the compiler can exploit the type informations to compute property offsets, remove runtime type checks and unbox values. Observe that dynamic, optional and concrete types can be mixed seamlessly; above for instance we have left the argument of the dist function dynamically typed, so that it is correct to invoke it with an arbitrary object as in t.dist({x=1;y=2}).

StrongScript strategy for program evolution is to first add optional types, catching and fixing unexpected local type errors; the programmer can then identify the parts of the code that obey to a stricter type discipline, and replace optional types with concrete types. Optional types act as a bridge to move values into the concrete world:

1
2
3
  var fact = func(x:!number) {return ...:!number}
  var u:TypedPoint = { dist = func(p) {...} }  
  var n:!number = fact(u.dist(p))
In the example, p has type any, and u points to a dynamic object with a method dist typed any -> any. However, u has been typed as TypedPoint; the runtime will ensure that the method dist respects the TypedPoint.dist signature any -> !number and will dynamically check that the returned value is an instance of class number. As a consequence, fact(u.dist(p)) is well-typed (the concretely typed function fact is guaranteed to receive a value of type !number) and the programmer, by specifying just one optional type, can invoke the concretely-typed function fact with a value that has been computed from the dynamic world.

The ability to have fine grained control over typing guarantees is one of the main benefits of StrongScript.


Last update: