Motivation to publish
My exposure to TypeScript has been limited, but is likely to increase in the future. I’m bound to change my current opinions as my familiarity with TypeScript grows. The reason that I decide to hit “publish” despite - or rather because of - my limited competence with TypeScript is twofold:
- For future self-reference: Archiving my current thoughts will allow me to reflect on my learnings and the development of opinions.
If TypeScript never clicked with you, you’re not alone!
Reprogramming (the brain) is more effort than learning something new from scratch.
Here’s why I perceive the learning curve to be steep:
Terminology and concept theory first
Learning TypeScript feels like “proper computer science”: One has to dig into terminology and grasp a bunch of concepts before being able to type seemingly simple every-day code snippets. For the self-thought web developer persona, learning about generics or magic terms like “type predicates”, “polymorphic
this types” and “distributive conditional types” requires effort and dedication.
Getting to know type errors
And that’s tricky.3 The best way to find problems is to know the name of the relevant TypeScript concept. Unfortunately, plenty of a beginner’s problems are not knowing about these concepts and when to apply them. So yeah, don’t skimp on the theory and learn the “TypeScript lingo”, or you won’t be able to phrase your problem. It doesn’t help that TypeScript has gotten vastly more capable with every version. It’s not uncommon to find answers that are far from current best practice, because they stem from TypeScript 2.x times. Secondly, there’s often multiple ways to approach typing a specific situation. Power and flexibility are good for experienced practitioners, but can be confusing and lead to bad choices with beginners.
Code readability: new verbosity and complexity
With my limited exposure to TypeScript, when I read TypeScript code, I’m always getting the same first impression: Everything feels harder to understand.
The code is more verbose than required for human reading. Some of the verbosity helps human readers to understand the code better too4, but some of it is just there to satisfy the compiler.
For me, TypeScript’s benefits have to offset downsides in reading and also my - at least initially - reduced productivity in authoring code:
Code authoring: helpful vs. interruptive
TypeScript code authoring is a two-edged sword:
On one hand there’s benefits from precise auto-completion and linting. These saves multiple seconds in frequent, small interactions from the get-go.
My flow when authoring code is constantly interrupted by trying to find ways to satisfy the TypeScript compiler.
Especially when that same code is working perfectly for users, it feels like work is put into a worthless satisfaction of a machine instead of to the benefit of users. Extra brainpower is spent thinking about the type system. I assume that past a certain skill threshold, these stumble-stones are gone: Once writing type syntax doesn’t need dedicated thinking, states of flow might be interrupted less, e.g. because jumping between code to understand interfaces is reduced.
Shift left: Quicker feedback loops on defects
Above, I bitched about loosing minutes to hours needed to write good types as a beginner. But even these hours might be quickly re-gained:
Certain type of bugs are less likely to go unnoticed, due to TSC errors. Better yet: When TSC catches errors, it does so right away, while authoring the code, and while being in context. There’s no code change iteration quicker than whilst in the editor. For those of us who are not practicing TDD, finding common issues of badly handled data before having written a single unit tests speeds up the time to having reasonable confidence that the code is working. I don’t have sufficient experience to know whether having TypeScript reduces the need to write a part of the unit test cases. If so, that would be another relevant time saving.
Any bug that ends up as noise in a monitoring tool like Sentry needs to be analyzed. That’s time consuming, since a developer looking at a long list of different exceptions in the monitoring tool shuffled together needs to jump between exceptions, and between exception details and code, which the developer has likely not written themselves - costly! After the 1st pass of analysis, the issue needs to be put into an issue management system, and triaged. Then, the bug needs to be solved. If that’s another person at a later time, there’s another round of “self-onboarding” into the faulty piece of code. These “issue management” round-trips and the cost of missing context tend to accumulate to hours, even for the most trivial null pointer. We should not conflate static type safety with runtime safety and correctness. But there’s a correlation. And having to deal less with defect handling crap is pure value.
A valid question to ponder is whether the type of quality issue prevented by TypeScript is the type of defect that is most costly in your program. In teams I worked in, most costly bugs tended not to be null pointers or other concerns of code correctness. Instead they were human mistakes not preventable with TypeScript:5 Overlooked edge-cases and forgotten use-case scenarios. They might not have been considered in the design, or lost in the translation to implemented code. These types of mistakes tend to be made consistently and are prone to group-think. They were overlooked by everyone, and hence forgotten to be tested.
Migrating an existing code base
If your team concludes that the expected gains in productivity &/ quality surpass the migration cost at the relevant time horizon, there’s organizational questions to answer, which impact whether and how a migration to TypeScript should happen: What people on the team know TypeScript best? How do we scale their knowledge to the rest of the team? Is structured training needed, or can we get away using existing processes like pair programming and PR reviews? What’s the expected output of the team in the time during the migration? Can we afford to move at a slower pace for the time of the migration? How do we migrate? Is the code base small enough to migrate everything at once? Or do we establish rules like that every new file should be TypeScript, or that every file that is changed should be migrated? What strictness do we want to enforce initially, and do we want to tighten it over time?
Getting started: easy and expected
When starting the migration, things start simple: Add a
Now the team starts to pay down the pre-assessed investment cost. The reduced productivity when authoring new code described above. Time spent on training and learning. And every time an existing file’s ending is changed to
.TypeScript, TSC will ask for more specific types and raise issues that need to be addressed manually.
The long tail
Then there’s the long-tail of considerations flowing from the decision to migrate to TypeScript:
Type definitions are extra code. How should that code be structured? Where should it be put? What naming scheme applied?
Only once a part of the system is statically typed, it becomes apparent with how much other code and systems it interfaces with.
- There’s team-internal code, like front ends / APIs / GraphQL servers,
- There’s systems external to the team, like APIs run by other teams, 3rd party dependencies like NPM packages,…
Ideally, to run the safest system, all these systems should be correctly typed, and types auto-updated as these interfaces evolve. Some of that is solved with extra work adding extra tooling, e.g. to:
- Transpile GQL schemata and OpenAPI specs to TypeScript.
- Validate and type
But you’ll run into plenty of areas where perfect auto-maintained typing is not easy to reach, or out of your power:
- You might not have resources to migrate the other team internal systems (yet). Boundaries to these systems will remain invalidated.
- Some of the used NPM packages might not ship with types. There might or might not be
@typespackages. You might decide to replace dependencies with similar dependencies which were built with TypeScript, which means that the migration to TypeScript spawns more (and more error-prone) migrations.
- The types shipped with (
@types) packages might be inconsistent with the actual implementation. There’s little more annoying than TSC screaming at you that the code is wrong when in fact the type definitions are wrong.
- Sometimes browser or web APIs are typed with a custom type. These types can have varying fidelity, and differ from specs. It’s “great” when the GQL library doesn’t accept your custom
axiosHTTP client, because
axiossupposedly fetch-compliant type definition is different than that of the GQL library.
- Your team can write the “Type contract” for 3rd party APIs which don’t come with specifications. But you might have an incomplete understanding of the 3rd party API’s behaviors and type it out incorrectly. And there’s nothing forcing the 3rd party to uphold compatibility with these types. The 3rd party might change API behavior any time in ways that might be incompatible with them. When reality at runtime differs, static typing is no help.
- If you’re working inside a mono repo with other teams, you’re either bound to keep all teams’ TypeScript version in lock-step, or risk TSC incompatibilities within the mono repo.
Don’t underestimate migration costs
The cost of this extended setup required before TypeScript’ benefits cover a broad part of the system is easy to under-estimate.
I’m not a fan of the extra NPM-heavy tooling required to help to type everything, and the tooling and process required to keep types up to date and in-sync either. These increase your system’s surface area, and - after the initial setup costs - mean additional continuous maintenance.7
The biggest one got to be change management though. Getting the buy-in. Coordinating the migration. Keeping everyone aligned. And, most crucially, ensuring that everyone is assisted in learning TypeScript. I’ve seen TypeScript forced upon a team by a loud, freshly-joined minority without any change management, and I can tell you, it ain’t pretty. Without proper investment into the people, a TypeScript migration is bound to incur costs like churn and burn-out, vastly exceeding the benefits.
Opinions on when to use TypeScript
Cases where I’m leaning against
But let’s say that the majority of the team is motivated to migrate an existing code base to TypeScript. I think there’s still cases in which not to migrate to TypeScript:
The 1st one is when there’s insufficient knowledge of TypeScript in the team. In this case I believe that going with TypeScript is going to be a hard road with high opportunity costs and plenty of early mistakes. There needs to be a few people with experience, who can up-skill the team.
Secondly, migrating an existing code base in a phase when the team can’t afford to slow down or halt feature work for a while. E.g in a start-up which didn’t start with TypeScript, which is still finding product-market-fit, and churning out features and changing things at a rapid pace.8
Thirdly, simple representational websites which would otherwise not require tooling, are an obvious “no”.
But I’d go further and claim that using TypeScript on the front end is generally lower in value than in other parts of the stack: Not all UI libraries / frameworks make typing easy9, and - in my experience - the impact of bugs related to type-mismatches in front ends tends to be limited. A null pointer in an SPA is not a glorious user experience, but can often be remedied with a reload - no big harm done. Front ends are inherently more fuzzy about strictness and correctness, since they deal with dynamic, user-generated content, invalidated inputs, and a big plethora of dynamic client capabilities and needs, be it in network conditions and reliability, OS system and browser (versions) and their idiosyncrasies, viewport sizes, input capabilities, needs for localization…10
Situations in which to use TypeScript
There’s situations in which I think that TypeScript is the right choice too:
When providing a programmatic interface to 3rd parties, that interface should be codified. That’s a clear “yes” in favor of using TypeScript when authoring an NPM package.
When working on an API, and/or interfacing with a database, using static typing also makes sense. In these cases, in- and outputs anyway have a defined type contract, and TypeScript types can usually be auto-generated with tooling. Ensuring that this type contract is kept in both directions through the code flow should usually not require too much work. The characteristics of being an API and/or interfacing with a DB probably entail most back end systems.
When building a large code base, and/or working in a large team or organization. Under these circumstances it’s unlikely that one person can have a good grasp of the full system in their head, so getting real time validation on the large count of interfaces is more valuable. Big code bases tend to be old, and hence experience developer churn over the lifetime, which further reduces the contributor’s knowledge about how the system works. This point would seem to cover most open source too, as (new time) contributors are foreign to the code, and come and go.
A question of personality?
Independent of “objective” factors influencing how much sense TypeScript makes in a team’s situation, the culture of the team is probably the biggest deciding factor in favor or against TypeScript.
If your first programming language was typed, you’ll always prefer that. If it was dynamic, you’ll always gravitate to dynamic languages.
People having “grown up” with types feel unsafe without them. And people used to dynamic languages feel less free when working with typed languages.
There’s something to this hypothesis of two different “schools of mind”. Maybe it’s not even about the upbringing, and instead about how risk-averse someone is?
I heard another job candidate who was used to TypeScript saying that he was “flying blind” when changing unknown code without types, claiming that the lack of types made the code hard to understand, and impossible to tell whether changes had bad side-effects. He was lacking a safety net that would at least warn him about mistakes. He was expecting the computer to tell him what’s allowed and what’s not.
Whereas I think that the act of coding consists of reading and understanding code before changing it. Even if I can place my cursor in the middle of an unknown function and have the computer tell me what the “interface boundaries” to the surrounding code are without me reading it:
I don’t want to write a single line of code without understanding how the surrounding works.
It’s the full context that allows me to build the new code in a way that’s “native” to the existing structure, to build the best version of the new code, or to even improve the structure whilst making my change. The full understanding is what allows me to come to creative or elegant solutions that go past fulfilling the pre-defined requirements that the new code needs to fulfill.
Factors boosting TypeScript
The best of both worlds
TypeScript is pushing further into the mainstream through simplification. The type annotations proposal would add a better, standardized JSDoc to ECMAScript. This would allow to conquer use-cases without build step and bake the core of TypeScript forever into the web platform. Once something’s part of the web platform, browsers stay compatible with it forever (a small risk for the ever evolving TypeScript syntax?). Without tooling requirement, TypeScript is bound to face less opposition by platform purists, and the friction to get started and scale into full-blown TypeScript will be vastly reduced.
The idea that the editor should provide type hints and warnings makes sense to me. A typed code-base provides the desired dev experience today. But I’m lazy - it feels tedious for me to have to provide types myself.
With recent launches of “AI” based code generation tools I wonder:
How hard is it to build “intelligent” LSPs, which can infer intended types from the code and find type issues and interface compatibilities by simulating fuzz testing?
That’s the future I’m in for!12 Dev tooling today is surprisingly primitive in light that its users are the ones using and capable of building it. The programs we build using the primitive tooling could not get away with such a bad UX. Intelligent LSPs to me sound like a more desirable and archivable intermediate goal compared to full autonomous code generation.
The points laid out in this post are likely to be much less relevant or even moot for someone experienced with TypeScript. ↩
Finally I can emphasize with Junior developers again. You can’t just paste your non-working code into Google. You need to understand your code well enough to be able to know the terms for the abstract concepts used by your non-working code. ↩
E.g. knowing the data shape of parameters of a function called from some other piece of code currently not in view. ↩
Your mileage will vary. When working on systems possibly impacting lives, or systems directly sealed to money flow, any code correctness issue is one too many. ↩
Akin to “Innovation tokens”. ↩
Maybe this inherent chaos is why the front end developers I know are more relaxed and’t don’t fall for the illusion that the own system can ever be perfect. Whereas the typical back end developer I know seems to believe in and strive for an unflawed, correct system, and be a stickler about it. ↩