Over a million developers have joined DZone.

Combining Static and Runtime Type Checking For Better Productivity

Using TypeScript to annotate static parts of your code for advanced editing, refactoring, and error detection.

· Web Dev Zone

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

Dynamically-typed languages enable interesting development workflows. The ease with which you can experiment and innovate in, for instance JavaScript, is extraordinary. But there is a catch. When your application and team grow, the inability to define contracts in the code makes the application fragile and refactorings expensive. Every time you undertake one of those, you see errors like “undefined is not a function”. This is a sign of a wrong object being passed in as an argument somewhere in the codebase. It is also a sign that you will have a painful debugging session. So dynamically-typed languages provide better dev experience in the short-term, but have problems in the long-term.

This is what systems like TypeScript aim to solve. In TypeScript, you can use types to annotate static parts of the code. This enables advanced editing, refactoring, and early error detection, which improves the dev experience in the large, without losing what is great about JavaScript – you can still experiment as easily as you could before.

TypeScript’s optional type system can get you very far. Painful debugging sessions are rare in a reasonably-typed TypeScript codebase, and this is fantastic!

There are certain things, however, that just cannot be checked statically.

Talking to a Backend/Web-Worker

Say you make an http call to the backend. TypeScript cannot statically verify that the fetched data is of the right type. So you just have to tell the type checker to trust you.

interface Movie { title: string; releaseDate: string; }

@Component({...})
class MoviesCmp {
  movies: Movie[];
  constructor(http: Http) {
    http.get("/movies.json".map(resp => resp.json()).subscribe(json:any =>
      this.movies = json // type checker cannot verify this
    );
  }
}

Interacting With Untyped Libraries

Another example where the type checker is helpless is when your application has to interact with untyped third-party libraries. Once again, the only thing TypeScript can do is to trust you.

interface Movie { title: string; releaseDate: string; }

function bestMatch(movies, query) {
  // some funky code we do not control
}

@Component({...})
class MoviesCmp {
  movies: Movie[];

  findBestMatch(query: string): Movie {
    return bestMatch(this.movies, query);
    // type checker cannot know if we are passing right arguments to bestMatch
    // nor does it know if the function returns a Movie
  }
}

Reflective Code

Finally, if you have some funky meta-programming code, where you reflectively transform or generate objects, most likely the type checker won’t be able to figure out the types.

function convertStringsToDates(obj) {
  const res = {};
  for (let k in obj) {
    if (looksLikeDate(obj[k])) {
      res[k] = parseDate(obj[k]);
    } else {
      res[k]= obj[k];
    }
  }
  return res;
}

final normalizedMovie = convertStringsToDates({title: "Star Wars: Episode VII", releaseDate: "Dec 18, 2015"}); // type checker does not know the type of normalizedMovie

Unhelpful Exceptions

In a reasonably-typed application, these limitations are why you can still see “undefined is not a function”. Or even worse, you may not get any error. Instead, the application suddenly starts behaving weirdly due to a value somewhere being undefined. This is not good dev experience. So let’s see how we can fix it.

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:

interface Movie { title: string; releaseDate: string; }

class MoviesRepo { // a facade object
  constructor(private http: Http) {}

  allMovies(): Observable<Movie[]> {
    return http.get("/movies.json".map(resp => resp.json());
  }
}

@Component({...})
class MoviesCmp {
  movies: Movie[];
  constructor(repo: MoviesRepo) {
    repo.allMovies().subscribe(m => this.movies = m);
  }
}

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:

App Architecture

Note, 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:

import {CheckReturn, objectLike, arrayOf} from 'runtime-type-checks';

class Movie { title: string; releaseDate: string; }

class MoviesRepo {
  constructor(private http: Http) {}

  allMovies(): Observable<Movie[]> {
    return http.get("/movies.json".map(resp => this.parseMovies(resp));
  }

  @CheckReturn({fn: arrayOf(objectWith('title', 'releaseDate'))})
  private parseMovies(resp:any):Movie[] { return resp.json(); }
}

@Component({...})
class MoviesCmp {
  movies: Movie[];
  constructor(repo: MoviesRepo) {
    repo.allMovies().subscribe(m => this.movies = m);
  }
}

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:

App Architecture with Boundaries

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 Arrangement 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.


Follow Victor on Twitter to learn more.

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:
typescript ,static ,runtime ,type checking

Published at DZone with permission of Victor Savkin, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}