Introducing ATTyC — type safety for AngularJS templates

We Make Waves
6 min readDec 7, 2017

--

Our main project at UVD, Limpid Markets, is an AngularJS app written in TypeScript. As anyone who’s worked with AngularJS knows, there are a huge number of pitfalls to avoid, one of which is a lack of type safety (or any other sort of safety for that matter) in templates. If you make a small typo, Angular will often just fail silently when it tries to render the template, without even warning you. Even if you have very good test coverage, bugs can often slip through into production.

After encountering a number of small errors with our templates in Limpid Markets, we decided we needed some way of statically analysing our template files to try and detect these errors. At the time, the only tool we were aware of to do this was html-angular-validate, a linter for Angular templates. We ideally wanted something a bit more strict, that could use our TypeScript types to verify that a template is correct. We couldn’t find any existing tools for this, so we decided to write our own. Thus, AngularTemplateTypeChecker (ATTyC for short) was born.

What is ATTyC?

ATTyC is a command line application which you can use to check the expressions (i.e. the bits in curly braces, directive attrs, ng-if etc.) in AngularJS template files are correct. It checks that each expression has valid syntax, and obeys all the type constraints you specify. For example, say you specify that variable n is of type number (we’ll cover how to do that shortly). If the expression n.toFixed(3 is found in your template, ATTyC will tell you it isn’t valid because of the lack of a closing bracket. Also, if ATTyC finds the expression n.concat([2]), it will tell you that you can’t call .concat on a number.

ATTyC needs to know about every variable you use in your template to effectively type check it. It needs to know, at minimum, the name and type of each variable. If you’re using the “controller as” syntax (and you should be), you should just need to give ATTyC the name and type of your controller. The way you do this is (loosely) inspired by the MVC Razor @model syntax. ATTyC looks for a comment at the top of the template, which we call the template’s “metadata”. This metadata contains an EDN vector of maps, each containing the keys :name and :type. You can also add an :import key, which is the path relative to the template that ATTyC should import the type from. This comment block will probably look something like this:

<!-- [{:name "ctrl" :type "Controller" :import "./controller"}] -->

One thing to note is that although ATTyC does it’s best to find any bugs in templates, it isn’t intended to completely prove that your template is correct. As far as I’m aware, this is impossible with AngularJS because scopes are so dynamic — for example a child scope can add properties to the parent dynamically. Nevertheless, we’ve found that ATTyC will catch 90% of bugs with your templates.

How does ATTyC work?

ATTyC is written in ClojureScript, which compiles to JS and runs on Node. This allows you to install it through NPM — just run npm install -g attyc, easy peasy. The basic flow is:

  1. ATTyC reads your template file
  2. It extracts the metadata
  3. It then extracts all the expressions in the template
  4. All the expressions are wrapped in closures to isolate them, and concatenated into one string
  5. This string is run through the TypeScript compiler

Predictably, this is harder than it looks on paper. There are several reasons for this. Firstly, ng-repeat and ng-options have their own "special" syntax, which aren’t documented well. I found lots of very strange edge cases with ng-repeat. Just to give you one example, x in (y = 1) && [1,2,3] actually works, and you can use y in bindings both within and outside the ng-repeat block (wat).

Another problem was how different scopes interact in templates. Child scopes can add or mutate properties on their parent scopes, and vice versa. You can also write expressions that declare variables anywhere in a template, and these expressions can be run at any time. This can lead to completely unpredictable behaviour, which is in general impossible to statically analyse. To get around this, ATTyC does not allow variable declaration anywhere outside of ng-init, ng-repeat and ng-options attributes, even though this is technically possible in AngularJS. Also, all initialised variables are treated as global, even though they’re actually not.

One other major problem I encountered is working out when Angular will treat an attribute as an expression. It does this for certain built in attributes, such as ng-if, but not all of them bizarrely. It also treats some directive properties as expressions – you specify this in the directive definition. However, this gets very confusing when multiple directives are on the same element, and their property names collide. ATTyC takes another shortcut here, and assumes that an attribute is an expression if it contains a variable you specified in the metadata, or if it is a built in Angular attribute that expects an expression. Although this can in theory cause ATTyC to incorrectly flag up errors, it means you don’t have to specify all the directives used in a template – which is a trade off worth making IMO.

An aside about Instaparse

Instaparse is a fantastic Clojure/ClojureScript library for building context-free grammars. It’s very easy to get started with, even if you don’t know what context-free grammar means (I still don’t ), and it’s extremely powerful.

ATTyC uses Instaparse to verify and extract expressions from templates. Originally, ATTyC used regexes for this, but ng-repeat and ng-options have their own grammars, and extracting TypeScript expressions from them is very difficult using regexes. Instaparse parsers are far easier to write, extend, and understand than regexes for this use case.

ATTyC also uses Instaparse to verify that the expressions themselves are correct. This basically involves writing a parser for the entirety of JavaScript, which is about 15 lines of code. This allows ATTyC to detect that expressions like [{y: 1] are wrong. (Note that if an expression in curly braces can’t be parsed, ATTyC errs on the side of caution and just ignores it).

Does using ATTyC really help avoid bugs?

As soon as we’d written the initial version of ATTyC, we tried running it on 3 of our smaller template files. Predictably, it highlighted a few bugs in ATTyC itself, and there were a number of incorrect errors. However, it did actually immediately pick up a couple of bugs in our templates that had gone unnoticed for years. It also highlighted one instance where a directive worked, but the types in the controller weren’t correct.

Personally, I’ve found using ATTyC during development gives me a lot more confidence that the templates I write are correct. It’s saved me a few times already, and allows you to spot errors faster than unit tests do.

Future Work

I believe ATTyC is a useful tool, but there are definitely a few areas for improvement:

  • Filters are not currently type checked. If you try and give an object to a filter that needs an array, ATTyC won’t complain. It would be nice if ATTyC could detect these kinds of errors, without too much extra configuration.
  • Performance — ATTyC could definitely be faster.
  • Better error messages. The error messages that ATTyC gives you are directly from either the TypeScript compiler or Instaparse, and it doesn’t tell you where in the template they occurred. This sounds like a bigger problem than it is, it’s usually easy enough to spot which bit of your template is causing the error. However, the error messages could definitely do with some improvement.

Alternatives

When I wrote ATTyC, I wasn’t aware of any other tools that do a similar thing. I’ve since heard about a couple of alternatives you can check out:

Like ATTyC, they both attempt to type check templates, but the approach they take is slightly different. They make different design decisions, which may make them more suitable for you.

I hope you’ve enjoyed this blog post, and it’s at least made you think about how you can add some type safety to your templates. ATTyC is on GitHub at https://github.com/davewm/attyc if you want to check it out. Feel free to open an issue if you have a question or you’ve found a bug, and PRs would be very welcome indeed.

Originally published at www.uvd.co.uk on December 7, 2017.

--

--

We Make Waves

We make digital products that deliver impact for entrepreneurs, startups and forward thinking organisations. Let’s Make Waves! wemakewaves.digital