Header image by Chris Leggat on Unsplash.
Another day, another heated “discussion” about how static typing in JavaScript is both the single greatest thing since sliced bread and the worst thing to have happened to humanity as a whole.
Let’s look into a recent dev.to post that has been stirring this debate back up again. I’ll try to clear out some misconceptions, and hopefully, take things in a level-headed manner.
Before I start, I want to change up some terms I used, especially the one in the title. Instead of referring to TypeScript specifically, I’d like to use the term “typed JavaScript”. Because there’s also another tool in the typed JS land, Flow, and I don’t want to leave Flow users out. After all, we have the same goal of enforcing type soundness/safety in our apps.
Another term that I would like to throw into the glossary is “dynamically-typed” JS, or “dynamic” for short. Despite what the original post wanted to make you believe, writing JS code without type annotations doesn’t mean that your code doesn't have types. A string written in dynamic JS still has a type of string
. So is a number
, boolean
, you get the point. You just don't have to explicitly express said types.
Yes, it’s longer to start writing statically-typed JS short-term…
I’m going to level with you: writing dynamically-typed JS is faster in the short-term. You might be surprised hearing that from a TypeScript advocate, but I’m being serious here. Really!
Let’s say you’re writing a Node.js library. If you’re writing it in dynamically-typed JS, you can write your library and publish it, all without going through any build tools. It’s that fast! For tiny libraries that do one thing, writing it like this is the most effective way because of the speed.
But now, let’s say you’re writing an entire backend in Node.js. It’s just a tiny API service with a couple endpoints. You have written your authentication system, middleware, and controllers in JavaScript. And since it’s a small endpoint with minor functionalities, you went with plain ol’ JavaScript.
Now, imagine that tiny API service balooned into a full-fledged platform API with thousands of code. Probably tens of thousands of lines of code. Then you realised that you found a bug in one of your endpoints. Oh dear! Unit testing didn’t catch it, so you had to spend hours to trace around your app, looking for the issue. Maybe even setting up breakpoints, or even doing the old-fashioned console.log
-driven debugging.
Then, you found the issue. Remember that one time you refactored that one middleware? You also changed the name of the exported function along with it. Sure, you had that middleware unit-tested, but your unit tests were only isolated to that middleware.
Then your eyes came across a file where you had that middleware imported. Of course. You changed the exported function name, but you forgot to rename the imports.
Hours of productivity lost just because of a typo or missing file!
…but the long-term effects are real!
Sure, you can also check mismatched imports with linting tools. But you might also want to rename a function — as well as updating the function name on all the files that import said function — all in the click of a button. After all, humans make mistakes, and missing things like this is not uncommon. TypeScript’s support for quick refactoring and find-and-replace support helps you deal with this. Therefore you can focus more on writing code instead of doing pointless find-and-replace by hand.
Static type checkers like TypeScript and Flow also help reduce the amount of bugs in your code by detecting errors like this during compile time. There’s some statistical proof to this, too. In general, using static typing in your JavaScript code can help prevent about 15% of the bugs that end up in committed code.
Sure, this will make starting out a project with TypeScript much slower, because you’ll need to define types, interfaces, and the like, in the very early stages of your app. But I’d argue that having you write implementation models, in the form of types and/or interfaces, makes you think about your app’s data structure early in the day.
This greatly improves confidence of your app in the long run. And when you use these types well, in many cases you don’t even need types, thanks to TypeScript’s control-flow based type analysis. The benefits of TypeScript on large-scale apps outweighs the trade-offs of the longer time to kickstart your TypeScript project.
Is this an investment that you would take in the future? It sure is for me, but I wouldn’t make any prior judgement for your apps. It’s still up to you to decide whether that investment is worth it.
You can adopt TypeScript incrementally
Maybe you’re already maintaining a medium to large-scale app that’s already written in plain ol’ JavaScript. And you want to migrate to TypeScript, but are afraid that the red squiggly lines will haunt you in your sleep. How would you go about migrating your code?
There are various guides in migrating to TypeScript. There’s one in Basarat Ali Syed’s awesome TypeScript Deep Dive handbook. I have also written a comprehensive guide here.
Another neat part of TypeScript is being able to infer types of normal JS files through JSDoc annotations, so if you write valid JSDoc annotations, and have JS typechecking turned on, it’ll be easy for you to migrate down the road.
Although admittedly, the migration experience is where TypeScript falls short. The reason I linked to third-party guides is — well — TypeScript does have an official migration guide, although it’s horribly outdated. The official documentation also makes hard assumptions that the user know something about statically-typed languages, so I wouldn’t recommend them to newcomers.
Though rest assured, the TypeScript team has been working on reworking the documentation, as well as a new handbook that will hopefully teach TypeScript a lot more progressively.
But what about dynamic, runtime values?
Admittedly, the TypeScript team has explicitly stated that extending static type-checking to the runtime is a non-goal for the TypeScript compiler itself. But in reality, we still have to handle these runtime boundaries. A common example to this would be reading a JSON output from an API, or consuming an HTTP request payload.
Since there’s a strong community backing towards TypeScript, the community has developed elegant solutions to this issue. There are tools like io-ts which you can use to determine runtime representations in TS. A suitable alternative to Flow would be flow-runtime.
Static typing and testing go hand-in-hand!
So far we’ve done a lot in making sure the type safety of our app with static types. Despite that, there are certain bugs static typing cannot catch. For a quick example, testing whether that toggle button displays its opposite state the right way when clicked.
I’m a fan of the Testing Trophy model by Kent C. Dodds. In his model, both linting/static analysis/static type checking and unit testing are located in the “base” of the trophy. This means that they’re both integral parts into building a testing experience that evokes confidence in your code. Hence I’d like to argue that both static typing and unit testing go hand-in-hand in helping you write code with less bugs!
Let’s put the toggle button example above into code. We’re using TypeScript as our static typing, and Jest + react-testing-library to test our code.
Here’s an example of said component, implemented in React.
import * as React from 'react'
interface ToggleButtonProps {
enabledText: string
disabledText: string
}
function ToggleButton({ enabledText, disabledText }: ToggleButtonProps) {
const [toggle, setToggle] = React.useState(false)
const handleToggle = () => {
setToggle(!toggle)
}
return (
<div>
<span>{toggle ? enabledText : disabledText}</span>
<button onClick={handleToggle}>Toggle</button>
</div>
)
}
export default ToggleButton
On the surface, it looks like static typing has done its job. However, if we take a look closer, we’re able to set a custom state text for our toggle button. Sure, TypeScript can check if the string we passed to the enabledText
and disabledText
props is a string. But that’s just half of the battle.
After all, if we’ve set our button’s enabled and disabled state is set to 'On'
and 'Off'
respectively, we want it to correctly show 'Off'
when it’s disabled, and 'On'
when it’s enabled. Not the other way around.
Since we already checked the types of our component and its props through TypeScript, we can focus on testing the behaviour of the button.
The following example uses Jest as our test runner, and react-testing-library as our React testing utility.
import * as React from 'react'
import { render, cleanup, fireEvent } from '@testing-library/react'
import ToggleButton from './ToggleButton'
describe('ToggleButton', () => {
afterEach(cleanup)
test('correctly renders the state of button', () => {
const { getByText, queryByText } = render(<ToggleButton enabledText="on" disabledText="off" />)
// Test the initial state of the button.
expect(getByText('Off')).toBeDefined()
expect(queryByText('On')).toBeNull()
// Fires a click event to the button.
fireEvent.click(getByText('Toggle'))
// Test if toggle state is correctly modified.
expect(getByText('On')).toBeDefined()
expect(queryByText('Off')).toBeNull()
})
})
Two things are happening here.
- Static typing provides soundness and improves the developer experience by detecting type errors and allowing developers to refactor confidently through great IDE tools.
- Unit testing provides confidence that our code behaves the way that it’s supposed to be used.
Let’s clear our heads
The original post contained a lot of subjective points, which was a shame because I’d love some objective reasoning as to why static types aren’t worth the time.
My rebuttal to the original post… also contains a lot of subjective points. But that’s fine! Because my goal in writing this post isn’t about going off about how one technology is “objectively better” than the other. I was trying to outline how one technology might benefit the users more than the other, and vice versa. And also find a commonality shared between both of them. Or at least, I tried to.
Instead of building inflammatory, subjective opinions disguised as “objective” “fact”, let’s approach things in a level-headed manner and understand that certain tools exists for certain reasons. Constructive criticism is a great power to improve all of us, regardless of which side you’re in on this debate.
Since I’m a front-end developer myself, a good example I’d like to pick would be the endless debates between the Big Three frameworks (Angular, React, and Vue), and why one is better than the other.
For example, Vue and React developers often went up in arms, writing senseless Medium thinkpieces about how one is better than the other. I’m a React guy myself, but I still understand that Evan You had his own issues to tackle with Vue, hence the issues that he solved being his framework’s key selling point. The most prominent ones being the learning curve and how easy it is to adopt.
The people behind TypeScript and Flow are smart enough to tackle one of their pain points in writing JavaScript. They want to write JavaScript code that scales in large-scale projects. And the way they approach that is to provide a static typing superset that ensures type soundness and safety, as well as improving productivity through developer tools that are enabled thanks to the power of static types. And for some people, it worked well. TypeScript and Flow are both running many medium to large-scale projects out there (including where I work), and I could imagine all the ways they enabled engineers to write code with less bugs.
TypeScript might be a waste of time for you, but it certainly isn’t a waste of time for me.
Don’t get me wrong, there’s nothing wrong in writing plain JS as well! Maybe you want to iterate faster on the early stages of the project, so you opted for plain JS instead of jumping into TS straight away. Maybe you want to get down and dirty with TypeScript right from the get-go. Both of these are fine. After all, it’s only you knows how to best develop your app. It’s only you who knows how to serve a better user/developer experience.
Because regardless of our choices of tools, languages, and frameworks (or lack thereof), it all ends up with a working app.
Right?
Top comments (38)
Yesterday, I read an article arguing that typescript was verbose and pointless.
To be candid, I don't use typescript frequently. But, I was shocked to read someone thought it was verbose. I mean, let's survey all languages. If the author of the article I mentioned thinks typescript is verbose, he's not had much experience with Java or C++ !
Geesh.
Yea I think people forget that in Java and C# you can’t equate instances that have all the same things. Look at this gross C# code:
^ now that’s verbose. In TypeScript anything that has the same data shape fits the interface, due to structural subtyping. So I can cut that code in half.
I think its very weak to compare to languages by trying to force the approach taken by one into another. You need to compare idiomatic solutions to the same problem to really understand them.
In strongly-typed object oriented languages, you need to be specific about polymorphisms. In general, I would like the compiler to stop me trying to pass (e.g.) an Order object to an User related process. To take your example, if I notice that a User is also a Person, I can write this;
Now, User inherits the SameName method and more generally, I can use a User as a replacement for a Person. I could have also used an "IName" interface if I wanted naming to be distinct from class hierarchy.
So, there is no code to "cut in half", and no chance that anything I didn't expect to be passed to the SaveName method will even compile. Also, if I ever change SaveName, no chance that code will break (at runtime).
You "can" (maybe more quotes are necessary) with type converters which you need to declare and implement in code hence the quotes.
I'm of C++ and C# background and when I read about such features in javascript I get the chills. Maybe I'm old school and I feel that structure similarities don't mean much. When I need them, I just interface them or implement the extra glue code. I'm all for strictness and clarity even if some extra coding is required. Extra coding for me means, that I implicitly enable the same option/functionality. I like scripting languages that are more flexible but I believe that each has its place and purpose.
Yea structural subtyping is really convenient but it is not as strict as nominal type system (like that of Java/C#). The TypeScript language devs are looking into adding opt-in for nominal types. I think it would be cool to have the option. I guess that’s one of the pros of TypeScript is that it gives your strictness but also some escape hatches. Kinda like C# will let you do crazy dynamic things via reflection.
I'm not sure that reflection falls under the same category but I would agree with you about the spirit of TypeScript you mention. This is also a big difference, like generation differences, between C, C#/Java, JavaScript. Each addressed problems based on that periods concerns, let them be software or hardware.
Are you sure the above code is C#?
I'm certain it is neither literally C#, nor idiomatic of the features the language provides.
Thank you for clearing up a lot of misrepresentated info from the original post.
Yes! It’s nice to see the pros of static type analysis properly represented.
But honestly, I’d just like to see people give the tooling a shot. Once you try it you just might love it. I know I do! :)
Speaking of tooling, I forgot to mention Babel's integration with TypeScript via
@babel/preset-typescript
on the post, which was a game changer indeed. For large projects already tied to the Babel toolchain, it's finally safe to include TS into the toolchain, while keeping all the great features of Babel. It also allows you to use the many plugins that the ecosystem provides (e.g.babel-plugin-styled-components
).Oh, and all
@babel/preset-typescript
does was strip out the type checking, type-checking is still done by the TS service (and is pluggable to a CI service). Which is really useful for those who want to edit, save, and preview/debug quickly without type errors constantly blaring at them. :)Don’t forget how awesome
Create-React-App —TypeScript
is. It just works! :)Fantastic post - there’s nothing to prove really. Anyone who says “X language / framework is a waste of time” is coming from a close-minded perspective and it’s really not useful as programmer to have that kind of outlook.
Being open to different paradigms and ways of coding is one sign of an excellent developer.
party time 🌟
I haven't learned it yet. Though saw many people who hate TypeScript, however, I think I will like it if I use it. Personally, dynamic languages make me sick.
Yeah I've seen that. That article really is the worst example of those kinds of posts. /:
It really screams "I used this for two weeks. Here are the minor inconveniences I encountered which proves it's an Evil Technology that should be avoided at all costs!!"
LOL I feel a bit of guilty and think it's my duty to defend them a bit.
I inherited some frontend code at the end of last year and as a backend engineer, I had some catching up to do to take ownership of that.
I made good progress learning my way around the code base. I started with small additions. Then I updated the build system and dependencies. After a few weeks of messing with this and getting quite frustrated by the amount of stuff that broke everytime I touched something, I added typescript support. At that point I was pretty sure lack of types were slowing me down.
Adding typescript made life vastly easier. Before I did that, refactoring was a pain and I had essentially no idea if my changes would actually work. After that, at least the parts I migrated to typescript (properly, with strict mode), became a lot easier to deal with and I started editing with a reasonable level of confidence.
After having migrated a few thousand lines of js to ts, the lines of code you gain in terms of verbosity is very limited. Mostly you end up with the same number of lines of code or at best a few percent new lines.
Mostly the annotations happen in lines that you would in any case have: method and class declarations. This adds a lot of value as it also documents what is expected. The extra lines you gain would be things like interfaces to add strong typing to e.g. objects you deserialize from an API. I'd argue doing this adds a lot of clarity and VS Code makes doing this stupidly easy. Just go CMD+. and add the declaration automatically.
But then, typescript is in my opinion nothing more than a gateway drug for something better. If you like it, there are much nicer languages. Some of them even transpile to javascript or to WASM (or both in some cases).
Nicely written!
I think there's a lot of good qualities for TypeScript that outweigh the bad qualities that the other article tried to point out.
TypeScript can be really useful for new frontend developers who know next to nothing about JavaScript. It seemed to work really well when I pushed hard to get our developers to use it on our React project.
Great point! In many ways TypeScript can be used as a tool for teaching Javascript.
I found TypeScript (v0.8 ... it was a while ago) to be pretty good.
One of its features was bringing ES6-isms to ES3 and ES5, and that was fantabulous! These days, everyone has ES6 (and later) at their fingertips. But it still delivers on static type checking, which eliminates a very large class of bugs, especially for large programs.
When I was doing TypeScript, I was looking enviously at CoffeeScript. But at that time, it looked like CoffeeScript's days were numbered. Now there is CoffeeScript 2, huzzah! (Of course, CoffeeScript isn't about type safety, it's about expressivity and succinctness.)
Programming languages are tools. Some are more suitable to a particular domain. If I were targeting JavaScript I'd be using... well, Elm. But if I were on a team, and the team was gung-ho on TypeScript, I would be fine with that.
I hope we get more support for algebraic datatypes. The union and intersection types are so close to the discriminated unions of F#...if they can gross that gap Typescript is going to become a much more powerful tool. The benefit of static types becomes less about catching careless mistakes and more about designing types that don't allow for any illegal states in the first place.
I hope this can help - dev.to/macsikora/more-accurate-the...
Typescript is not bad at all, especially compared to all other typed languages.
For applications that need to have a high level of integrity, I think it's a must.
Having said that, I do not use typescript in any of my personal projects. I love the simpleness (and the apparent and inevitable sloppiness) of JavaScript.
Nice Article!