Cookies
Diese Website verwendet Cookies und ähnliche Technologien für Analyse- und Marketingzwecke. Durch Auswahl von Akzeptieren stimmen Sie der Nutzung zu, alternativ können Sie die Nutzung auch ablehnen. Details zur Verwendung Ihrer Daten finden Sie in unseren Datenschutz­hinweisen, dort können Sie Ihre Einstellungen auch jederzeit anpassen.

Engineering

Embracing AtScript Typing
Minuten Lesezeit
Blog Post - Embracing AtScript Typing
Steffen Schildknecht

In compiled, statically typed languages such as Java or C++, a compiler, along with rigid syntactical structures, will take care of that. It will make sure that a method that is supposed to work with an object of a specific type will only ever receive that type of object. Dynamically typed languages often do not have that kind of luxury as they are missing a dedicated compile phase and present more challenges for static analysis to catch type errors. One of these languages is JavaScript.

Enter AtScript

AtScript is a small set of optional extensions to the ECMAScript 6 (ES6) language specification, which is the basis for current and up-and-coming JavaScript interpreters. AtScript is developed by the AngularJS team at Google and serves as the implementation language for the upcoming version 2.0 of the popular frontend framework. Despite its origin, AtScript’s extensions are framework agnostic and can be used in every kind of JavaScript application. The proposed extensions are as follows:

  • Type Annotations allow the definition of types and interfaces based upon those types to simplify reasoning about an application system.
  • Field Annotations are intended to emphasize the definition and structure of classes and types used in an application.
  • Metadata Annotations and Type Introspection allow adding additional meaning to parts of an application, which other parts of the application or a framework can reason about.

This post focuses on Type Annotations and Field Annotations to explore the possibilities of runtime type checking in JavaScript applications.

Getting Started with AtScript

AtScript implementation development is split between two projects:

  • The Traceur transpiler, a popular choice of transpiling ES6 code to ES5 targets, has been enhanced to optionally support AtScript’s language extensions.
  • Assert.js is an implementation of a runtime type checker which is called by transpiled AtScript code to define and compare types. Assert.js can be swapped with other implementations to support different kinds of type systems.

While the AngularJS team has provided an AtScript Playground repository, it has not been kept in sync with the current state of Traceur and Assert.js. You can find an updated version in our repository here, which also includes instructions to get up and running along with a set of specs for the different enhancements.

Basic Type Usage

The syntax for adding type information to valid ES6 code is derived from a proposed type system for ECMAScript 4. Simply putting a colon and a type after the thing you are annotating is enough:

var aNumber:number = 1;

The code above says that it expects the value being assigned to the variable aNumber to be aNumber. So what happens when we assign something different?

var aNumber:number = 'definitely not a number';

We get an exception - at runtime:

exception - at runtime

This mechanism is the basis for developing AtScript with type annotations - during development you will not have to backtrack through several layers of code to find out where things went wrong. You are informed immediately if a value does not fit the expectations - or contract - of your application.

You can do something similar when defining function parameters:

function doubleIt(input:number) {
  return input + input;
}

doubleIt(2); //= 4

doubleIt('two'); //= Uncaught Error: Invalid arguments given! -
                 //  1st argument has to be an instance of {name: "number"}, got "two"

Or when defining a function’s return value:

function doubleIt(input):number {
  return input + input;
}

doubleIt(2); //= 4
doubleIt('two'); //= Uncaught Error: Expected to return an instance of {name: "number"}, got "twotwo"!

By using type annotations we are essentially able to express a contract for a function. A developer can take a look at a function’s definition and reason about its inputs and outputs. The contract is enforced by checking inputs and outputs at runtime and generating exceptions if an expectation is not met. You can turn off these checks for production builds in order to not degrade performance.

Defining Types

There are a few built-in types in Assert.js: void, any, string, number, boolean.

void is mostly useful for checking that a function does not return anything.. any will literally match anything including undefined.

Using type annotations becomes increasingly interesting once we plug our own types into the system. If we decide to use an ES6 class as a type, the system will check whether a given parameter is an instance of that class.

class Nut {}
class Grass {}

class Squirrel {
  eat(food:Nut) {
    console.log('The Squirrel has eaten.');
  }
}

var squirrel = new Squirrel();
squirrel.eat(new Nut()); //= 'The Squirrel has eaten.'
squirrel.eat(new Grass()); //= Uncaught Error: Invalid arguments given! 
                           //- 1st argument has to be an instance of Nut, got {}

This is nice, but it may prevent you from using type annotations in places where the expectations for an input are based upon its interface rather than upon its ancestry. E.g. you want to make sure that an input responds to a specific function call. This is where custom types can be of value:

class Duck {
  quack() { return 'QUACK!' }
}

class Goose {
  quack() { return 'HONK!' }
}

class Tiger {
  roar() { return 'ROAR!' }
}

// Define custom type ‘Quackable’
var Quackable = assert.define('Quackable', function(quackableType) {
  assert(quackableType.quack).is(Function);
});

var logAnimalQuacking = function(animal:Quackable) {
  console.log(animal.quack());
}

logAnimalQuacking(new Duck());  //= QUACK!
logAnimalQuacking(new Goose()); //= HONK!
logAnimalQuacking(new Tiger()); //= Uncaught Error: Invalid arguments given!
                                //  - 1st argument has to be an instance of Quackable, 
                                //  got {} - undefined is not instance of Function

The function passed to the assert.define call is evaluated to check whether the given argument adheres to the type Quackable.

When presenting AtScript at NgConf, it was also stressed that one might want to use structure type checking for validating Ajax Responses. This can be accomplished using a custom type and the assert.structure helper:

var ProductResponse = assert.define('ProductResponse', function(productResponse) {
  assert(productResponse).is(assert.structure({
    id: assert.number,
    name: assert.string,
    price: assert.number
  }));
});

var productResponse:ProductResponse = { id: 1, name: 'TV', price: 499.0 }; //= OK

var anotherResponse:ProductResponse = { id: '1', name: 2, price: '499.0' };
  //= Uncaught Error: Expected an instance of ProductResponse, got {id: "1", name: 2, price: "499.0"}!
  //- {id: "1", name: 2, price: "499.0"} is not instance of object with properties id, name, price
  //- "1" is not instance of number
  //- 2 is not instance of string
  //- "499.0" is not instance of number

You may also want to use generic types. Generic Types allow you to express types that are containers for values of certain types:

// Building upon the previous custom type example...
var logAllAnimalsQuacking = function(animals:Array) {
  for (var animal of animals) {
    console.log(animal.quack());
  }
}

logAllAnimalsQuacking([new Duck(), new Goose()]);              //= QUACK! HONK!
logAllAnimalsQuacking([new Duck(), new Goose(), new Tiger()]); //= Uncaught Error: Invalid arguments given! 
                                                               //- 1st argument has to be an instance of {type: Array, argumentTypes: [Quackable]}, got [{}, {}, {}]

Although the implementation of checking such types in Assert.js is not yet finished, we have implemented a version of it in a fork to showcase the feature.

Field Annotations

Field Annotations are a nice feature for expressing the type structure of a class. They allow us to use a shorthand for typed getters and setters of specific class fields:

class Point {
  x:number;
  y:number;
}

var point = new Point();
point.x = 10;    //= OK
point.x = 'Ten'; //= Uncaught Error: Invalid arguments given! 
                 //- 1st argument has to be an instance of {name: "number"}, got "Ten"

So, should I use this?

AtScript type checking is a welcome extension to the ES6 language. While one could use the Assert.js library on itself to implement type checking manually, having type requirements directly available as a syntax feature should make type declarations effortless while programming and make programs easier to reason about when examining code. The added runtime checks could prove extremely useful while developing in big teams or with multiple teams.

Despite being a promising development and mostly usable, the AtScript type checking implementation is not finished yet. While Traceur seems to have most of the language features in place, Assert.js still needs work to support (more) generic types. So, while it is interesting to experiment with and maybe use for smaller projects we would advise you to wait for AtScript to stabilize until you use it in production code.

Resources

Podcast:

Videos:

Ihr sucht den richtigen Partner für eure digitalen Vorhaben?

Lasst uns reden.