The Value of Complex Types
Author
KalleDate Published
I still remember the first time I learned about more advanced types. It was at university, and the course used generic types in Java to teach us about the complex parts of data structures. Back then, it was overwhelming, and it was hard to see the value of it. Especially the notation using single letters like T
or K
didn't make it easier to understand. This is, of course, typical for academia, and in hindsight, I fully get that the abstract concepts need abstract variables. But I could see this as a reason why many people are scared of complex types.
Today, I see more and more generic types, especially in TypeScript, with descriptive names, which makes it easier to understand. Even on the abstract level, it's now more common to see Type
instead of T
and Key
instead of K
. But still, the complexity of the type system is there, and even though the barrier of entry got lower, the complexity can be seen as an overhead during development. So why should we care?
The Benefits of a Strong Type System
In my opinion, the benefits of a strong type system can be broken down to one simple point: Safety.
A type system allows the compiler to check your code, and with the rule set defined in the type system, your computer can point out many bugs while you are still writing the code. This perfectly fits the "fail fast" principle, which is a key concept in software development. You want to see errors as soon as possible, so the immediate feedback loop in your IDE is as best as it can be. This can also be seen as a form of unit testing, e.g., all function inputs are tested if they are of the correct type, and you can be sure that no unexpected null
values are passed down.
For web development, this was not available for a long time. JavaScript started as a very simple language, with the goal to be directly executable in the browser. Coming from compiled languages like Java, C#, or even C can be quite difficult. Suddenly the IDE feels more like a text editor, and you are on your own to remember all the function signatures and the types of the variables. Luckily, TypeScript is available, and with the current adoption rate, it is the de facto industry standard.
The Rise of TypeScript
The first public release of TypeScript was in 2012 (version 0.8), and for the first few years, it was a niche project. Back then, it was also not foreseeable that Microsoft would become so important for the web development community. My first contact with the language was in 2015, with version 1.4. I was immediately hooked because finally, my computer was able to help me again with my code. Even though WebStorm was a great IDE for JavaScript, coding always felt dangerous, especially when refactoring in larger codebases.
In 2015, Facebook released Flow, which tackled the same problem but with a different approach. Flow didn't rely so much on annotations and tried to infer the types automatically. However, from my perspective, TypeScript was still a lot easier to use because of the tooling and the community.
I think the tooling was also the main reason why TypeScript became so popular and is now the industry standard for web development. With the release of the Language Server Protocol (LSP) in 2016, the integration of TypeScript in all major IDEs was possible, paving the way for widespread adoption.
Over the years, it was interesting to see how the community adopted TypeScript. In the beginning, there were many loud voices against the "boilerplate," but at some point, the same people started to tweet about always using the "strict" config. The last domain regularly complaining about TypeScript is the web libraries. A common talking point is "use TypeScript for your app, but not for library code." Perhaps it's still possible to improve the type system to make it easier for dynamic use cases, but I think dynamic libraries are complex by definition, so it won't ever be easy to type them.
Example: Type Safe Navigation in React Native
Recently I had the problem that the navigation in a react native app was not as stable as I wanted it to be. The navigation was done with the react-navigation
library, which is a great library, but not very opinionated about how to use it. Each screen can define the parameters it expects, but it's up to the developer to keep the types in sync with the actual implementation. My solution was to create a function to generate a static navigation config with a navigate
function that takes a path and the necessary parameters. Another function generated from the static config provides access to the screen parameters based on the path. This creates a single source of truth for the navigation and provides feedback about broken navigations at compile time.
The basis for this solution is a tree structure to define the screens and their parameters:
This allows a URL structure with with screens on the same level and each screen can have nested screens as children. Each screen can define the query parameters it expects. Parameters can be optional or required and can be of type string
, number
, or boolean
. URL parameters are defined as key in the tree structure. An example tree could look like this:
These type definitions are pretty simple so far, but with the next helper type the complexity increases. The goal is to extract the route configuration from the tree and a URL path:
This type is not used directly, but it enabled targeting every screen config in the tree with a URL path. The following example shows how to get the config for the profile screen:
With the route config for a URL path, the next step is to extract the parameters and their types:
Similar to the RouteConfigForUrl
type the new definitions allow to target the parameters of a URL:
Additionally to the query parameters the navigation tree can also define path parameters:
These helper types are independent from the navigation tree and just provide all segments in a URL starting with :
.
The last part of the ground work is one helper type to extract all possible URL paths from the navigation tree:
This generates a union type of all possible URL paths in the navigation tree:
The last step is now to generate the type definition for all possible app links, so the combination of all possible URLs with their parameters:
This allows the definition of the AppLink
type, which can be used for type save navigation:
If you want to see the full example in action, you can use the TypeScript playground
Conclusion
The example above shows how complex it can get, but also how powerful the type system in TypeScript is. Coming up with this solution took a lot of time, but as the navigation is a core part for the app the gain in safety was worth it.
In general it is very important to find the right balance between premature optimization and too much technical dept. In the concrete example I introduced the type safe navigation after the app already had more than 70 screens.
For a project like this be sure of the behavior you want to describe, and don't spend too much time to over-engineer the solution. But also take your time to learn what TypeScript can do for you, learning the ins and outs of the advanced types opens up a whole new solution space for your projects.
Mailing List
If you want to receive updates on new posts, leave your email below.