A late adopter’s guide to TypeScript
Everybody and their mother uses TypeScript these days, so I guess I have to, too. This is the guide I wish somebody showed me a few years ago about how good TypeScript is!
All the errors these fancy type systems tend to fix for us are errors anyone could spot. The safeguards the type system put in place are limiting, an extra pain in the butt, and an all-around waste of time. Right?
In the interest of science, I introduced TypeScript on a greenfield design system we were building. It seemed like a good match – no legacy weirdness to coerce into a type system, and a ton of great APIs to create.
Today, I’m full-on sold on TypeScript. I’m what you’d call a late adopter of these weird, yet wonderful type definitions people kept hassling me about. If you’re still unsure if types are for you – let me tell you a bit about the reasons that made me fall in love.
API based development
I’ve always obsessed about the APIs of the code I create. It’s the very presentation of my code to the rest of the world. Other people will interact with the interfaces I create, and it’s important for me to think them through before I implement them. How many arguments should my function accept? What should they look like? What should it return?
Previously, there wasn’t really anything “forcing” me to think about how I would model my APIs. They typically got their shape along the way. This technique both had its good and bad sides.
On a positive note, I felt very productive while coding and refactoring the API along the way. I wrote a lot of code since I changed my mind and implementation several times from when I started until I ended. I tested out different possibilities by writing them out and hitting their limitations before I changed course. I thought I’d absolutely loathe not being able to do this in TypeScript.
Turns out, thinking things through before you start the implementation job is an incredible exercise that boosts the quality of your work a huge amount. Your design becomes clearer, understanding usage patterns is now a prerequisite, and you end up writing much less code in the end.
So start writing out your interfaces or types before you start implementing – the value it provides is just astonishing.
Library support turns out to be amazing
If you’re using something that doesn’t have its types provided somewhere, you can write your own type definitions or just opt out of type coverage altogether. This is probably something you’ll do in reverse – first opt out, then write your own local types, and finally create a pull request to DefinitelyTyped to help the community out.
TypeScript + modern React = 😍
While we’re speaking of library support, I want to highlight React. It’s what I write every day, and its interoperability with types was definitely a success factor for my transition to types.
Previously, I’ve had some troubles creating types for so-called higher-order components. These were functions that accepted a component, and returned a new version of that component, just with more capabilities. I still shiver thinking about them.
Luckily, HOCs are as good as superfluous in modern React. After the introduction of Hooks, sharing logic and behavior like this has become an almost trivial task – and even easier to write good type definitions for. Remember – custom Hooks are just functions with arguments and return values – and that’s all nice and typeable.
Another thing is classes – they tend to introduce a lot of generics and other advanced type patterns thanks to inheritance and other object-oriented features. I honestly haven’t written a new class component in about a year – and writing types for function components is just as easy as for regular functions.
Add on to this great type definitions and documentation in the React library itself, and you have one happy camper right here.
Fewer tests with more value
I’ve written an enormous amount of unit tests in my life. When writing React, I often wrote tests to make sure my components didn’t crash and burn whenever they got a null or undefined value. With TypeScript – all of those tests just go away.
Since all of these tests now are a language feature that can be checked compile-time, your feedback loop goes from several seconds to an instant – an improvement that does wonders for your productivity.
In addition, the tests you do end up writing tend to focus on usage patterns instead of cringy edge-cases only a QA engineer would think about. Instead of testing for impossible scenarios, you end up writing test cases for what the user can end up doing.
Keeping it simple
Types may sound daunting, and they were to me as well. If you dive into a codebase created by TypeScript “experts”, you often end up seeing a lot of advanced generic types or features you’ve never heard of. I’m here to tell you you don’t have to use those features.
In most of the code I’ve written, I’ve only used regular type aliases and specified either built-in values or specific values like strings. It works like a charm, and it’s nice and readable for any type newbie stumbling into your code-base later on. Sure, type guards, inheritance, and discriminated unions are effective things to have in your toolbox down the road – but you don’t have to use anything you’re not familiar with. Stick to the basics, and you should be fine.
Another thing I want to mention is that you don’t have to go from zero to hero with your type coverage either. TypeScript gives you outs like the any type, which lets you keep certain “complicated” parts of your code untyped while you get your feet wet on the shallow end.
Safe refactors and inline documentation
The one feature I knew I would love out of the box was the improved support for safe refactoring and auto-completion while writing code. With modern editors like Visual Studio Code or IntelliJ IDEA, you get a ton of great tools to help you write and rewrite code much quicker than before – with more confidence as well.
Another cool feature is that you can easily add code comments that show up as inline documentation in your editor of choice. This will definitely help your future self understand what that “normalizeInput” function actually does, without having to dive into the source code.
Added value, without the hassle
After trying this out for about six months, I’m head over heels in love with the added value I’ve gotten from adding TypeScript to my tech stack. I’ve gained a ton of confidence in my code, and it has enabled me to write better code with fewer tests in the same amount of time. At the same time, it didn’t bring any of the issues I thought I’d stumble into.