Types have a bad reputation for making code harder to read, adding unnecessary ceremony, and in general complicating things. In this blog post I’d like to show that a type system done right can help make code more readable and toolable without constraining the expressiveness of the language.
DOCUMENTATION FOR HUMANS AND MACHINES
First, I want to show how type annotations can be used as documentation for both humans and machines.
Let’s look at this function jQuery.ajax()
and pretend that we are not familiar with it. What kind of information can we get from its signature?
jQuery.ajax(url, settings)
The only thing we can tell for sure is that the function takes two arguments. We can guess the types. Maybe the first one is a string and the second one is a configuration object. But it is just a guess and we might be wrong. We have no idea what options go into the settings object (neither their names nor their types) or what this function returns.
There is no way we can call this function without checking the source code or the documentation. Checking the source code is not a good option – the point of having functions and classes is to be able to use them without knowing how they are implemented. In other words, we should rely on their interfaces, not on their implementation.The only option we have here is to check the documentation. And there is nothing wrong with that. Except that it takes time.
Now, contrast it with a typed version.
ajax(url: string, settings?: JQueryAjaxSettings): JQueryXHR;
interface JQueryAjaxSettings {
async?: boolean;
cache?: boolean;
contentType?: any;
headers?: { [key: string]: any; };
//...
}
interface JQueryXHR {
responseJSON?: any;
//...
}
It gives us a lot more information.
- The first argument of this function is a string.
- The settings argument is optional. We can see all the options that can be passed into the function, and not only their names, but also their types.
- The function returns a JQueryXHR object, and we can see its properties and functions.
This bring us to my first point.
Types serve as documentation for the programmer.
In addition, this information enables advanced autocompletion, navigation, and refactoring capabilities. Having such tools is almost a requirement for large projects. Without them programmers are afraid to change the code, which makes large-scale refactorings almost impossible. As Anders Hejlsberg puts it, you can build a large application in a dynamically-typed language, but you cannot maintain it. Without advanced tools a large code base is always in a semi-read-only state.
Types enable advanced tools.
TYPES PROVIDE A CONCEPTUAL FRAMEWORK FOR THE PROGRAMMER
A good design is all about well-defined interfaces. And it is much easier to express the idea of an interface in a language that supports them.
For instance, imagine a book-selling application where a purchase can be made by either a registered user through the UI or by an external system through some sort of an API.
As you can see, both classes play the role of a purchaser. Despite being extremely important for the application, the notion of a purchaser is not clearly expressed in the code. There is no file named purchaser.js
. And as a result, it is possible for someone modifying the code to miss the fact that this role even exists.
function processPurchase(purchaser, details){
}
class User {
}
class ExternalSystem {
}
It is hard, just by looking at the code, tell what objects can play the role of a purchaser, and what methods this role has. We do not know for sure and we will not get much help from our tools. We have to infer this information manually, which is slow and error-prone.
Now, compare it with a version where we explicitly define the Purchaser interface.
interface Purchaser {
id: int;
bankAccount: Account;
}
class User implements Purchaser {}
class ExternalSystem implements Purchaser {}
The typed version clearly states that we have the Purchaser interface, and the User and ExternalSystem classes implement it. This brings us to my next point.
Types are useful for defining interfaces/protocols/roles. They provide a conceptual framework for the programmer.
It is important to realize that types do not force us to introduce extra abstractions. The Purchaser abstraction is present in the JavaScript version of the code, but it is not explicitly defined.
I’ve spent most of my career programming in Ruby and JavaScript, and I find these languages extremely powerful and flexible. But after working on sizeable apps written in these languages, I came to the conclusion that the lack of interfaces pushes developers toward building tightly coupled systems.
In a statically typed language boundaries between subsystems are defined using interfaces. Since Ruby and JavaScript lack interfaces, boundaries are not well expressed in code. Not being able to clearly see the boundaries, developers start depending on concrete types instead of abstract interfaces. And it leads to tight coupling. It is possible to build large decoupled systems in these languages, but it requires a lot of discipline.
STATICALLY TYPED LANGUAGES?
It may seem that I am advocating statically typed languages, but I am not. The arguments against a mandatory static type system are still sound.
- It complicates the language. Writing a mock library in JavaScript is an exercise that almost everyone can undertake. Doing similar stuff in Java is far more difficult.
- It constrains the expressiveness of the language.
- It requires type annotations even when they are not desirable (e.g., in a domain specific language).
What we want is something that combines the power of a dynamically typed language with the benefits of having explicit interfaces and type annotations.
OPTIONAL TYPE SYSTEMS
An optional type system does not require using type annotations. Type annotations are used to help you communicate your intent and get better tooling.
Optional type systems are forgiving. You do not have to satisfy the type checker. Quite the opposite, the type checker is there to help you find typos, search, and refactor. You do not completely rewrite your program to make the types work. If you are prototyping something, and duck typing works, just do not use types. Use them only when they add value.
It would not be completely fair to say that optional typing is only good stuff without any drawbacks. It still adds some complexity to the language (nothing compared to a mandatory static type system though).Also, to fully see the benefits of such a system you need more sophisticated tools than a plain text editor. But because of the reasons I outlined in this blog post, I think it is worth paying this price.
TYPED JAVASCRIPT
I wrote the original version of this blog post some time ago, and called it “I Wish Ruby Had Interfaces”. Back then I was a Rails developer. And I was extremely frustrated by working on a very large Rails code base, where some refactorings were taking months to finish. There was no hope Ruby would get an optional type system, so it was just a rant.
This time it is different. The JavaScript community is embracing the idea of optional types. TypeScript has become a robust tool used in production at many companies. AtScript, and Flow are new projects in this area. I am really excited to see what typed JavaScript community will look like in a few years.