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!
I’ve spent most of my professional career advocating for JavaScript. The flexibility provided by its dynamic nature makes it perfect for both quick prototyping and high-quality production software. I’ve been using it for making simple interactions, single-page applications, blazing-fast APIs and easy-to-understand shell scripts – and I’ve never really thought twice about its lack of types. Well, at least up until now.
To be honest, I’ve been increasingly frustrated with the never-ending supply of new advocates for types in my dynamic little language – with Flow fanatics and TypeScript bros each stumbling over their own bombastic arguments over how types are the future, and how writing type-free JavaScript is nothing short of irresponsible and – well – stupid.
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?
At the same time, the incredible surge of people advocating for types in JavaScript – and TypeScript especially – was a bit off-putting. Many of them were people I otherwise agreed with and respected – and I rarely saw people returning to plain JavaScript after a night of types. It was like… once you go typed, you stay forever hyped?
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.
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.
One of my biggest fears when moving to TypeScript was the lack of types in the JavaScript ecosystem. Turns out, this wasn’t an issue at all in 2019. The few packages that don’t come bundled with type definitions, have them readily available via the community curated DefinitelyTyped type repository. Over 6400 at the time of writing! I even provided a pull request myself to fix the only bug I ever found there.
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.
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.
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.
As an example, take a suite of unit tests that checks that your function handles all kinds of inputs. In JavaScript, we would need to verify the behavior of passing all the falsy types (null, undefined, 0, “”, NaN and false), and all the truthy, but invalid, types. We need to make sure programmatically that our code handles all sorts of weird argument types. In TypeScript, though, the type system provides us with the guarantee that any argument passed to our function will be of a predetermined type. If we try to pass something else – even several files away – our code won’t compile, and we’re unable to ship the code to our users.
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.
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.
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.
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.
I hope this article makes you give TypeScript a try – and if you tried it a few years ago – a second go. It’s an incredibly powerful addition to JavaScript that I don’t want to live without anymore – and I bet you won’t either.
All rights reserved © 2024