Static typing for the dynamically typed guy
It’s been a while since I’ve used a programming language with some kind of type checking. My transition from the C++ world to the Ruby one was difficult at first. That changed quickly as I became fluent and recognize the productivity benefits of a language with a REPL and just enough meta programming to be dangerous. In my world static typing had died. It was only getting in the way. Fact is, I’d always had the luxury of working in small teams.
Recently I’ve started to come back around to the other side. Type systems don’t have to be verbose and clumsy and can often be very powerful, just take a look at Haskell. They can also make a big difference on big, fast moving teams with diverse levels of experience and varied product requirements. Like any tool they can get in your way if used improperly. Used well types can be great in helping you make large code changes by “leaning on the compiler”. They can provide an additional level of documentation. They can even help you eliminate bugs.
Use the most appropriate type
It’s very easy to write highly functional programs in a statically typed language using only a handful of the built in types. The Java String class for instance is well supported by the language, and is very powerful. It can be tempting to use it to represent all kinds of domain specific knowledge, especially because Strings are often used to represent information coming from an external source.
But code that only uses a small set of types is taking limited advantage of the type system. If all types are the same then the type declarations provide limited documentation. It’s easy to mix and match values and produce invalid programs. Often the more appropriate type will include behaviour that is specifically relevant to its use cases.
When choosing the most appropriate type, one should be conservative. This means choosing the type that most accurately represents the data, but also the type that offers the smallest interface. This includes when referring to a type and also when instantiating it. An interface is more conservative than an implementation because it offers no behaviour. A Java primitive is more conservative than its boxed equivalent because it is incapable of representing the absence of a value (i.e. null). A sum type is more conservative than a union type because it is capable or representing fewer values.
Convert to a more appropriate type as soon as possible
At the frontier of any application there is a boundary between the code and the outside world. Where inputs such as network traffic, file data or keystrokes are converted from their native form into application data. This layer is sometimes called the trust boundary. Outside this trust boundary exists a wild west of data: unstructured, malicious, invalid or even corrupt.
Information enters through this boundary via simple primitives like integers, arrays of bytes or strings. Validating and then converting this information into a more appropriate type at the trust boundary prevents problems being propagated through the code base. Code bases that don’t take this strategy often end up being larger as more code has to be written defensively and processing has to be duplicated.
Consider adding a type
Often the types available in your language are insufficient to represent information relevant to your domain. This does not mean that you should settle for the simplest type available. Consider adding a type that is named appropriately to describe what it contains. Create as little as an interface as needed to make the type usable for your domain. Domain specific types can be invaluable when requirements change. Even if a type does not offer any behaviour adding a value type to hold it can aid in readability and ease of refactoring.
Types should be small, simple and immutable
When creating new types a good strategy is to keep them a simple as possible. A good rule of thumb is that a type should have a single responsibility. Smaller types are easier to compose together to provide value and tend to change less frequently. In general you should try to create types that cannot be mutated since this makes code harder to reason about.
As always with any set of guidelines there are exceptions to the rule. There are times when you’ll need to sacrifice these rules to achieve certain goals. For instance: immutable types can often be less performant than mutable ones. One could also take these rules to extremes and end up with an explosion of types. There is no harm in reminding the reader to be pragmatic. Nevertheless these rules if followed should result in a code base that can change more easily while remaining correct. That reveals the absence of certain types of bugs. That is easier to understand.