TypeScript: Safety in the Absence of Types - Angular 2

July 27, 2016 0 Comments typescript, angular 2

TypeScript: Safety in the Absence of Types - Angular 2

Separate Your Application and External Code

First, you need to separate our application code from the code talking to the backend or untyped-libraries, like this:

There are good reasons to do this. First, the backend code can be written by another team, and, as a result, can be changed without you even knowing it. But even if you know about the coming change, the repository object becomes the only place you need to patch. So you change different parts of the code base independently.

Once you separate your application code from the interactions with external systems, this is what the architecture of your application will look like:

Note that by doing this you also separated typed and untyped code. This means that an incorrect object can only get through one of these repositories. To prevent this you need to verify that the objects returned by these repositories and facades are what you expect.

Adding Runtime Checks

To show how you can do it, I built a small library for adding runtime type checks. You can find it on github and on npm. And this is how it can be used:

The CheckReturn decorator wraps the parseMovies method and verifies that the parsed value is an array of objects with title and releaseDate. If any of these fields is missing, it will throw.

The Runtime Type Checks library provides a few decorators for checking parameters and return values. It can do instanceof type checks out of the box, but can be customized to do structural checks, as shown above. I won't go into details in this blog post. But if you are interested, read more information here.

Back to our example. By decorating MoviesRepo with runtime checks, you change the architecture of the application to look like this:

You protected all external boundaries of your application, and since your application is reasonably-typed, you should not receive "undefined is not a function" or other cryptic messages.

Instead, when the backend API changes, the application will throw RuntimeTypeCheck: Expected Array of {title:string, releaseDate:string}, got [{title: "Star Wars: Episode VII", usReleaseDate: "Dec 18, 2015"}]. The releaseDate field is missing., with the stack trace pointing to the exact place where the mismatch was detected. This exception tells you the cause of the error and the place where it can be fixed.

Additional Questions

Does it affect performance?

Runtime-checks are primarily a development time feature. They give the developer more confidence that their code works. And when the code breaks, the developer gets helpful error messages. Having said that, the checks are performed only at the application or library boundaries, which hopefully are not crossed very often. So you can keep the checks running in production for better error reporting.


Does it affect testing?

As with everything, there are trade-offs. Runtime checks are great for acceptance tests or some dev mode in which you run your application. But unit tests are a different story. Runtime type checks make isolated testing and mocking a lot trickier. So if you rely on those in your unit tests, you probably should disable the checks in your unit tests.


Can we simplify structural checks?

Nominal checks (instanceof checks) are automatically handled for us by the library, but structural checks are not. So you have to specify a function that will do the checks (e.g., arrayOf(objectLike(...))). To simplify such check you can either use some library for defining structural types or use compile-type tools generating checks based on interfaces declarations.


Isn't this setup similar to gradual typing?

It is similar, but not the same. Gradual type systems ensure that everything inside the boundary is sound and cannot throw "undefined is not a function". The soundness allows the compiler to trust type annotations and do optimizations based on them. This is not the case in Flow or TypeScript.

Summary

Dynamically-typed languages, like JavaScript, provide great dev experience in the small. You can experiment with new approaches and build interesting solutions in days or even hours! But there is a problem.

How do you scale it to large code bases and teams?

This is what systems like TypeScript solve. They improve dev experience in the large without losing what is great about JavaScript. But they can only go so far because certain things cannot be checked at compile-time.

Runtime type checks at the application boundary in combination with static-type checking of the application itself provides the best dev experience.

TypeScript: Safety in the Absence of Types - Angular 2


Tag cloud