Typescript has seen a surge in popularity in recent years. Indeed it is one of the fastest growing popular languages. The reason for TS’s popular appeal is that it’s strict typed reformulation of javascript, one of the world’s most popular languages and the one which is deeply baked into the core of the web. Javascript as you may already know uses a loosey-goose y dynamic typing system which simply is not ideal for many programming contexts. JS plus strict typing is an attractive combination for developers seeking to build more robust and scalable web applications.
For all its advantages, typescript also has its drawbacks. Choosing it for your project is not always a no-brainer. Like any programming language or dialect, it is better for some use cases and worse for others. So it’s helpful to weigh the merits of TS rather than take it as a given that it is simply the next evolution in JS. Some of the advantages, as it turns out, can also be disadvantages depending on the use case.
What follows are some thoughts on Typescript after having used it extensively over the past couple years. Software engineering is both an art and a science . So while how languages work is a fact-based all or nothing question where you are either right or wrong, there are also more subjective nuances to when, where, and why to use tools where informed opinion rather than technical necessity has a place.
Let’s start with the obvious. Typescript allows you to specify java-like types for variables and objects. (In fact, TS uses Java’s type system.) It allows you to do this both during declaration of the variable, or to define the data shape of named objects through the specification of interfaces. So I can have a function:
function doSomething(param1: number, param2: string){
// something
}
Where I specify the function’s parameter types inline at declaration. I can also define a more structured type consisting of type primitives through an interface : '
interface DataObject { prop1: string; prop2: number; }
And specify that as the type for a parameter or variable:
function doSomething(param1: DataObject)…
I will get to the specific reasons this typing scheme is nice to have for JS shortly, especially with respect to the pratfalls of vanilla JS’s dynamic and implicit typing system. For now, the most obvious reason is that it ensures predictability and regularity in how data is passed around the app. Strict typing not only buttons down the code and ensures more robust standardization, it also tells the developer what to expect.
One advantage of explicit typing is that it tells you about your data. By specifying the types, TS eliminates some guesswork and makes for cleaner code. The more I started to use TS, the more clearly I started to think about the data moving around the app and what was supposed to go in and out of various functions simply because I could tell what to expect by reading the code. I could tell just by looking at it that for this function I should pass a string and get a number out. And it helps debugging when TS notifies you that an output does not match the expected schema. If a result is supposed to have certain required properties but is returned with them missing, you know something has gone wrong. By enforcing an explicit typing regime, TS encourages you to maintain a self-documenting ledger of all the typed elements in your code that provides a handy reference when you need to look up what data is required. Happily, this self-documenting attribute comes for free as a byproduct of simply writing code in typescript as intended.
It might be said that loose typing leads to loose thinking which leads to flabby, buggy code. TS helps you tighten up your thinking and your code by creating an explicit model of your data . With vanilla JS, this data model exists, at best, only in developers heads and at worst only in theory somewhere in the aether because nobody stopped to actually define it. Data models are for other languages , JS tempts you to believe. It’s okay to be lazy. TS forces you to do more work but it’s worthwhile to have an explicit model of your data. This extra work lends itself to more disciplined coding practices therefore better end products.
One of the most infamous drawbacks of vanilla JS is that it does runtime type checking, and has very loose and forgiving rules about type errors. In short, JS allows you to compile code that has non-breaking type errors, and it even does dynamic type casting and treats the occasional number as a string and so on. TS in contrast checks at compile time, meaning that errors that might lead to problems in production can be nipped in the bud. What TS does here is force you to future-proof your code. While the code may indeed be able to compile with those type errors and even run without breaking—for now— there may very well be some unexpected future contingency in which that type error JS breezily lets by breaks the code. Then it’s a hassle having to rummage through the codebase to trace the origin of the problem because in the meantime the complexity of the code grew out of proportion. TS embraces the principle of fail quickly and fail loudly. Typescript encourages tighter control of what your code will do at runtime by ironing out the details at compile time.
With the caveat that it is difficult to master. If you know JS, you know much of TS. It’s a bit of a freebie in this respect for a developer who knows JS. TS is still JS under the hood. When it comes to combining types and creating more nuanced nested interfaces, TS can get a little hairy.
TS does not force you to use it beyond the subtle point that differentiates it from vanilla JS. Including a tsconfig file with your project defines rules for what TS will do. The config file allows you to turn off some of what you might dislike about TS or lets you be a stickler about catching the particular issues you’re paranoid about and are adamant about banishing from production code. Specifically the compilerOptions
attribute allows you to specify what you want TS to do at compile time. This configurability leaves it up to the developer to decide what they want to prioritize . Such customizability allows you to minimizes the frustrations of TS and maximize its benefits for your specific project needs or personal tastes.
JS has a somewhat ignominious reputation as a slapdash, messy and hazily thought out language. Over time JS has grown more standardized and refined, but there are components to its conceptual DNA, specifically its typing system, that throw many developers through a loop. (null
is an object? NaN
is a number?). A practical example of these oddities is JS’s dynamic type casting, which requires implicit knowlege of rules and syntax that may compromise the codebase. So if you don’t remember to use strict equality ===
JS will evaluate ‘1’ == 1
to true. You might ask yourself: why do I need this? You also have to remember that the expression ‘1’ + 1
will be treated as string concatenation and evaluate to ’11’. Yet why not arbitrarily treat it as addition and evaluate to 2? Why should I have to remember that +
here means concatenation rather than addition? JS employs +
both as the addition and concatenation operator, but since you can’t subtract, multiply or divide strings all the other arithmetic operators used in the above case will cast the string as a number so for example ‘1’ - 1
gives 0. So this type casting breaks basic math making it largely useless if you wanted to dynamically prioritize numbers over strings. TS will either teach you practices that avoid these quirks or warn you about them when you do it. Apples will no longer be compared to oranges.
Once you get used to it TS just feels more robust and powerful than JS. Using it makes you feel more confident and secure in the code you write. Again that’s because from my experience, TS makes you think more about your code. This alone might be the major selling point in favor of it. TS just like a more mature evolution of JS. Be that as it may TS still has its drawbacks.
This is less about the language itself and more about the human side of development. For projects that need to move fast or who have developers used to vanilla JS, there can be a lot of downtime when using TS, time that could be spent actually developing features. The more strict your tsconfig, the more you will encounter annoyances. A draconian tsconfig will abort compiling on even the most marginal of TS errors. That is another way of saying the more you actually use TS the more frustrating it gets. You have to stop and think about your data. Which is good practice in its own right, but if one has to move fast, TS can be time-consuming. Though of these hangups can be mitigated depending on how the tsconfig file is set up a large enough team is bound to have disagreements over what is needed. Some will resent a given setting. Depending on how it is configured, typescript can give you an esoteric complaint that stops you from compiling your code. TS’s ruleset can feel bossy and overbearing, preventing you from doing things the way you prefer.
It can be frustrating when one knows this particular type error is objectively inconsequential given the specific circumstances and context in which it gets triggered, yet TS stubbornly refuses to compile. Such restrictiveness can limit developer autonomy. That may reduce potential human error, but it can also diminish creativity and flexibility in problem solving. The tsconfig dictates how the codebase should be written, for better or worse. It’s a matter of debate of course whether what is good for the codebase-in theory-is also good for the coders—or for the deadlines, in practice. Different teams may have different pain tolerances and needs. The calculus of trade-offs between fancier code and extra frustration is sometimes a balancing act. From my experience there is bound to be complaints and disagreements about what rules should be enforced.
Devs who need to keep moving often notoriously abuse TS’s <any>
type, which assigns a value any possible type. <any>
feels like an escape hatch when you don’t want to think about typing data. Abusing <any>
defeats the purpose of a strong typing regime, but it’s not uncommon to come across it. Just expect that you will start to see <any>
crop up with some frequency in proportion to which the strictness of your tsconfig ties developer’s hands, and the degree to which the difference between the current time and the deadline approaches zero. <any>
will be sure to crop up.
Some developers like JS’s type system and find that rather than inviting uncertainty and volatility it provides flexibility, especially for code that runs in the browser and operates on the DOM. JS was originally developed solely to run on the browser and to make HTML more interactive and event-driven. This is a role for which rigorous typing was reasonably thought to be unnecessary , possibly because it was assumed that much of that business would be handled on the backend and JS was supposed to be concerned with DOM manipulation. JS was originally intended to operate only on the DOM which, as a logical model rather than a programming language, doesn’t have a formal type system . As browser functionality grew more complex and frontend frameworks more sophisticated, JS suddenly acquired a lot more responsibilities when it came to handling data.
The main problem with dynamic typing is that it assumes implicit knowlege of the rules of JS, knowlege which exists only in individual developer’s heads and therefore cannot be counted on to be reliably and evenly distributed as the people behind a project change and the level of knowlege fluctuates as they come and go. By virtue of the fact that such knowlege is not embodied in the codebase itself, awareness of those rules cannot be guaranteed with consistency . Yet there are situations, particularly on the frontend, where it is safe or even valuable to allow numbers to be cast as strings and so on and one intends this behavior and plans for it. Flexibility has its perks. There are also situations where it is isn’t, especially on the backend, which is why Node without TS is increasingly seen as a questionable omission. Typescript exists to mitigate the human error involved in potentially compromising judgment calls.
Dynamic, implicit typing is also labor-saving. It offloads the time-consuming overhead of having deal with types by implicitly leaving that up to the engine. Projects of a smaller scale or with less sensitive data requirements may not warrant the use of TS.
TS is both a syntactic extension of JS and a special ruleset. JS’s dynamic typing allow you to decide when a number must be compared as equal to a number (using the triple equal sign strict equality operator ===
or loosely compare strings and numbers for equality ==
. If one wants to, it’s possible to write type-proofed JS, though it would require a lot of conditional boilerplate code to compare value types using the typeof
operator and such. Some projects may call for certain sections of the code that need this kind of typing security while other can be less strict. (One could, of course, combine TS and JS files in the same project as TS is transpiled into JS.) The point is that using TS demands a commitment. And this is where TS’s configuration settings are self-defeating if you find yourself disabling many of TS’s rules. If there is anything you don’t like about it, you can simply turn it off. If you find yourself doing that too much however, that is simply an argument in favor of not using typescript. If your tsconfig file is just an excuse to escape what annoys you about TS, then perhaps it’s time to reevaluate what role it plays in your stack.
This brings me to my last point about typescript. There seems to be a tendency among developers to think of strict typing as the standard in computer science objectivity. Without strict typing, code is loose and “subjective.” If you have strict, explicit types, your code is more “tangible” and definite, and therefore, in a way, more legitimate. The ideal here is to put all cards on the table, for there to be no implicit knowlege, or explicit ignorance, which might be overlooked.
It goes back to the art versus science debate. One could say that computer science is to physics what software engineering is to mechanical engineering: the iron-clad laws of the former constrain the whats possible for the latter in absolute terms, but so long as you work within those constraints there are virtually infinite ways to approach a problem. There may be no one-and-only way to implement an optimal solution because the space of possible implementations is so vast. Accordingly you would need an indefinite amount of time to compare an indefinite amount of possible implementations.
Best practices are not unyielding laws but heuristics derived from countless hours of experience which promote code patterns that approximate those regions of “design space” where good solutions tend to be found. Nevertheless what is best practice is what works best for a particular project and team and its specific needs and requirements, not what is objectively and ideally the case. For typescript’s shift in popularity to be more than just a trend, it has to be justified.
Everything that can be built with TS can be built with JS. The fact that TS is configurable is proof that it brings opinions, albeit well-informed ones, rather than facts about what’s best. It’s not necessarily true that what it suggests and recommends is flat out better in all cases than plain old JS. So don’t feel compelled to waste time and add to your frustration just because TS is slick and shiny if the project does not call for it.