I support the hypothesis that for any non-trivial software project the following holds. Over a software’s lifetime, the time and effort it takes to navigate, comprehend and debug existing code and the software system as a whole greatly outweighs the time and effort it takes to write it in the first place. Put differently, the time and effort of introducing a change is frequently dominated by understanding its context and not by actually changing the code itself. Optimizing for the activity with the lion’s share of the time is therefore only reasonable. This article focuses on how tooling that leverages static type information helps to achieve this goal.

Photo by Uroš Jovičić on Unsplash

Advantages

Excellent Navigation

The importance of navigation is hard to understand unless you have experienced both great and awful navigation yourself. The most useful navigational features seem completely innocuous on the surface—but they make a tremendous difference.

Precise autocomplete suggestions enable discoverability, exploration and guidance. There is no need to have API documentation open in a separate browser window or wade through the source code in order to find out how to use an element. By entering the dot character after the receiver element a little window pops up with all the available methods, fields and, sometimes, custom actions. All of this information is precise and context-aware and therefore smart. By scanning the entries in the popup with their signatures and inline documentation, insight into the receiver’s capabilities and its intended usage is quickly gained. A guess can be taken if a certain capability exists by typing out the supposed name. And the popup answers right away. The empowerment of this feature cannot be overstated.

enable discoverability, exploration and guidance. There is no need to have API documentation open in a separate browser window or wade through the source code in order to find out how to use an element. By entering the dot character after the receiver element a little window pops up with all the available methods, fields and, sometimes, custom actions. All of this information is precise and context-aware and therefore smart. By scanning the entries in the popup with their signatures and inline documentation, insight into the receiver’s capabilities and its intended usage is quickly gained. A guess can be taken if a certain capability exists by typing out the supposed name. And the popup answers right away. The empowerment of this feature cannot be overstated. Precise ‘Find Usages’ enables exploration and comprehension. From personal experience, the most promising approach to understand the role of an element (e.g. a class or a function) is to find out how it is used by the rest of the code. Applying ‘Find Usages’ on the element is the most efficient way to achieve this goal. The process can be repeated in a “depth-first search” manner in order to understand the role of other related elements. This way a whole subsystem can be comprehended relatively fast.

enables exploration and comprehension. From personal experience, the most promising approach to understand the role of an element (e.g. a class or a function) is to find out how it is used by the rest of the code. Applying ‘Find Usages’ on the element is the most efficient way to achieve this goal. The process can be repeated in a “depth-first search” manner in order to understand the role of other related elements. This way a whole subsystem can be comprehended relatively fast. Precise ‘Jump to Definition’ enables exploration and comprehension. ‘Jump to Definition’ is the dual of ‘Find Usages’. When interested in an element’s implementation or simply needing to understand an element’s surrounding context, ‘Jump to Definition’ is invaluable, in particular with third-party libraries.

Mistake Prevention

Static typing restricts the set of otherwise syntactically correct programs. To the uninitiated this might sound like a bad thing — far from it. In practice, this restriction prevents many mistakes that would otherwise manifest only in certain situations at run-time. These mistakes tend to have the unfortunate quirk of creating “explosions” far away from the root of the problem. Debugging these problems is hell.

Refactoring

When changing something structurally about the code the compiler and the IDE will point out all usage sites that need to be adapted. In many more basic cases, like renaming a class, the IDE will automatically adapt the rest of the code for you. If the tooling makes you feel confident about refactoring you will do more of it. And that is good.

Self-Documentation

The signature of a method communicates a lot about the expected inputs and the output to the reader. The type system makes sure that this information is correct—unless intentionally circumvented. Structures are explicitly described instead of implicitly. In general, names of local variables can be shorter as their type already conveys quite a bit of information.

Illustration

Consider the following code snippet.

function f(p) {

//...

}

Assume that the parameter p is always going to be an instance of a specific class at run-time. Can you infer this fact by analyzing the text of the above snippet? You cannot, of course, and neither can the tooling. The parameter p would need a type annotation (or you would need something like a whole program analysis which has many practical downsides). This example illustrates the main reason why all the navigational features as well as refactoring and self-documentation are much less useful and imprecise in non-statically typed languages.

Now, some popular non-statically typed languages have or will introduce optional type annotations. This is great! However, the cultural problem of writing “untoolable” code remains to a great extent. Consider the following method from a hypothetical AWS S3 library.

// Expects accessKeyId to be specified in config

function createS3Client(config: Object | Map<String, String>) {

// ...

}

Even with type annotations: If there is no sense of toolability—because the environment does not encourage it—annotations themselves are a drop in the bucket. The comment cannot be exploited by tooling either. What if you misspelled accessKeyId as accesKeyId? What if the library then implicitly picks up the access key identifier that you set as an environment variable but forgot about? Everything might work locally. Yet, you might observe a failure on production. In a statically typed environment the appreciation of toolability is greater. At least, the allowed configuration keys would typically be enumerated syntactically. Together with support from the IDE you would have spotted your mistake in an instant.

Conclusion

The implications of the mentioned advantages are profound. Not only do they have positive effects on the individual programmer but also on the ecosystem as a whole. Unfortunately, popular statically typed languages and their culture used to have inadequacies which lead to an image problem that continues to this day. These inadequacies, however, were separate from the points listed in this article and are mostly eliminated today—in parts thanks to the competition from other (non-statically typed) languages and ecosystems.

Further Reading

Here is a list of substantial links sharing similar views.

Further Notes

This section is optional and contains additional thoughts.

The case may be made that there are other means than static typing to achieve some of these advantages. On the other hand, it seems that static typing has a very good ROI that other solutions lack.

The notion that static typing is primarily about making mistakes impossible instead of tooling is widespread. A reduction in mistakes is a very nice side-effect of static typing — as is increased efficiency of the generated code — but it is not its main appeal for the pragmatic programmer. The reason is that there are many classes of mistakes that a typical type system will not catch. It cannot prevent you from wrong reasoning either. It is possible to devise a static type system that lets you encode many more properties about your program: See, for example, Dependent Typing. The price is highly reduced programming ergonomics.

Static typing can rather be seen as a means of making implicit links between syntactic elements explicit and therefore discoverable without the need to run the program. These links can be picked up by tooling like IDEs and lead to the advantages mentioned in this article.

A static type system in itself is not strictly necessary for great tooling. Smalltalk did not have a static type system and yet its users still rave about the fantastic tooling. The caveat is that the IDE must rely not only on the code’s static structure but also on its run-time environment. Today, the languages with the best tooling all have a static type system. The reason is straightforward: A static type system simply enables many tooling features.

The argument can be made that, at the end of the day, a good programmer can write good code in any environment and a bad programmer can write bad code in any environment. While true in a sense this argument seems to be a way to shut down discussion. A static type system and good tooling cannot prevent somebody willfully circumventing it. But, at the very least, it can guide the programmer to a better solution. A good programmer can write good code in any environment but he’ll most certainly write even better code in a better environment.

Some people make the argument that static typing is bad for prototyping. But the comparison is often made between wholly different frameworks and ecosystems. In my opinion, non-static or static typing itself does not influence prototyping significantly.

If you scout the web for videos and discussions from Dart’s and Kotlin’s creators around 2012–2015 you will find many similar arguments to the ones from this article. Dart received a lot of flak for chosing a non-sound type system (for a special case with generics). Some even suggested that therefore the type system is completely useless. The creators made the point that this is absolutely not the case.

Java is a very toolable language. Not only is it toolable in principle but it actually has great tooling, for example in the form of Intellij IDEA. There are many obvious but also more subtle reasons. A primary one is Java’s static type system together with its structured notational design. Java’s popularity cannot be disregarded either, in the sense that it would not have been worthwhile to put all this effort into its tooling without it. Granted, Java is not a particularly “sexy” language. Fortunately, there are alternative JVM languages like Kotlin that reuse the Java ecosystem and tooling while being a significant improvement over Java the language.

There are valid arguments against static typing. This is a topic for another article.

The following are additional links/sources with interesting points of view.