Tossing TypeScript

Adam Nathaniel Davis - Jul 27 '20 - - Dev Community

I don't need TypeScript. There. I said it. Honestly, it feels pretty good to finally assert that on the record. And if we're all being honest with each other, you probably don't need it, either.

My loyal readers (both of them) know that I've been diving into TS fairly heavily over the last several months. Those loyal readers (both of them) also know that I've been running into a few... headaches. But I've finally reached a point where I just have to admit - to myself, and to anyone else who cares to listen - that the whole push toward TS just feels misguided.

This doesn't mean that I've written my last line of TS. My employer seems to be fairly dedicated to it. So, by extension, I guess I am too. But I can't claim, with a straight face, that TS provides any real benefits. In fact, I've found it to be an unnecessary burden.

If this sounds like the rantings of an angry and entrenched greybeard, I suppose that would be fair. But consider this: I hadn't written a single Hook until February and I was increasingly exasperated by all the Hooks/FP fanboys who wanted to shout down any use of class-based React components. But now, 100% of my development is in full-fledged functional programming using React Hooks. So my point is that - I'm stubborn, to be sure. But I'm not completely set in my ways.


Alt Text

TypeScript's Type "Safety" Is Illusory

I've started to wonder how much TS devs even think about runtime issues while they're writing code. I feel like there's this misplaced, near-religious faith bestowed upon TS's compiler. The irony here is that, if you have any experience writing in strongly-typed, compiled languages, you know that "it compiles" is a common JOKE amongst devs.

When I was doing Java and C#, we'd be on some kinda tight deadline. Some dev would push a branch at the 11th hour. And he'd say, "Well... it compiled." To which we'd respond, "Ship it!!!"

Obviously, we didn't just "ship it". The joke is that getting code to compile is the lowest possible standard. Saying that your code compiled is like saying that an athlete managed to remain upright during the entire match.

Umm... Yay?

But in TS, sooooo much effort is poured into getting that magical compiler to acquiesce. And after you've busted your tail making all the interfaces and partials and generics line up, what have you achieved? You've achieved... compilation. Which means that you haven't achieved much at all.

It'd be fair to wonder how TS is, in this regard, any different from, say, C#. After all, even C#, with it's strong typing and robust compiling is vulnerable to runtime issues. But here's why I think it's so much more troublesome in TS.

Most frontend applications have no real data store. Sure, you can chunk a few things into localStorage. And the occasional app leverages the in-browser capabilities of tools like IndexedDB. But for the most part, when you're writing that Next Great React App (or Angular, or Vue, or... whatever), you must constantly rely on a stream of data from outside sources - data that can only be properly assessed at runtime.

When I was writing a lot more C#, it was not uncommon for my apps to run almost entirely in a walled-garden environment where I could truly control the database formats, or the returns from our own internal APIs, or the outputs from our own proprietary DLLs. With this kind of certainty at my fingertips, I'd spend copious time defining all of the data types my app expected. And in those environments, it was often true that, if my code properly compiled, it probably was pretty close to being "ship-worthy".

But when you're cranking out that next Unicorn Single Page Application, most of your critical data probably comes from outside the app. So the comfort of knowing that something compiled is... little comfort at all. In fact, it can be borderline-useless.


Alt Text

Code Is Only As Good As Its Interfaces

No, I'm not talking about TS's definition of an "interface". I'm not even talking about the true-OOP concept of interfaces. I'm talking about an interface as:

Any point in your application where data is exchanged with another application. Or, any point in your application where data is exchanged with another portion of your own app.


Once your app grows beyond a dozen-or-so LoC, you're no longer writing a single app. You're writing dozens of them. And eventually, hundreds or even thousands of them. This happens because we break our code into many, many, many smaller, more easily-digestible bites. If you're an "OOP type", you call these "bites" classes, or methods, or packages. If you're more of an "FP type", you call these "bites" functions, or components, or modules. Regardless of terminology, the effect is the same.

As a body is comprised of billions of semi-autonomous actors (cells), an app is comprised of hundreds, or even thousands, of semi-autonomous programs. So the quality of your app isn't so much predicated on the brilliance of your individual lines of code. Instead, the app's usefulness and hardiness are generally determined by how well all those little "pieces" of your app manage to talk to each other. Screw up the interface between two parts of your app (or between one part of your app and some "outside" data source), and your spiffy little app will suddenly look shoddy and amateurish.

What does any of this have to do with TypeScript? (Or even, JavaScript?) Well, I'm going to drop a radical concept on you:

Type "certainty" has little value inside of a program. But type certainty is critical at the interfaces between programs.



Alt Text

Bad Handshakes

Let's consider the havoc that can be wreaked by sloppy interfaces. Let's imagine that you need to generate random IDs throughout your application. You might write a function that looks something like this:

const createId = (length = 32) => {
  let id = '';
  const alphanumeric = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9'];
  for (let i = 0; i < length; i++) {
    let randomNumber = Math.floor(Math.random() * 35);
    id += alphanumeric[randomNumber];
  }
  return id;
}
Enter fullscreen mode Exit fullscreen mode

On the surface, this isn't a particularly "bad" function. We can use it to generate IDs of any arbitrary length - but by default, it will generate IDs containing 32 characters. Assuming we don't need true cryptographic randomness, the IDs generated by this function should work just fine for our app. But there's a problem...

There's a default value set for length. That's helpful. Assuming we want IDs that are of a default length, it allows us to call the function like this:

console.log(createId());  // ET6TOMEBONUC06QX9EHLODSR9KN46KWC
Enter fullscreen mode Exit fullscreen mode

Or we can call it like this:

console.log(createId(7)); // MSGFXO6
Enter fullscreen mode Exit fullscreen mode

But what if we throw a 'monkey' into the works by doing this?

console.log(createId('monkey')); // [empty string]
Enter fullscreen mode Exit fullscreen mode

This... could cause some problems. Potentially big problems.

'monkey' doesn't actually break the function. It still "runs" just fine. But it doesn't produce an expected result. Rather than receiving some kind of randomly-generated ID, we just get... nothing. An empty string.

Given how critical it can be to have valid, unique IDs in most apps, the generation of "IDs" that are nothing more than empty strings could cause significant issues.

You see, the interface for createId() (i.e., the function signature) allows us to pass in nothing at all, or any value for length - even if that value is not a positive integer. But the logic inside createId() contains an implicit expectation that length will either be a positive integer, or it will be undefined (in which case, the default value of 32 will be used).

This is where I often hear people say something like, "This is my program and I know all the places where createId() will be called. And I know that I'll never pass in some stupid value like 'monkey'." And that might be true. But even if it is, that's no excuse for crappy code.

You shouldn't create forms that will "break" if the user provides bad data. And you shouldn't create functions (or methods, or components, or classes, or... whatever) that will "break" if another programmer invokes them with bad data. Period. If your function only works properly because you always call it in the "right" way, then it's a poorly-written function.

In my experience, "handshakes", that happen all over our apps, are a major source of bugs - sometimes, nasty bugs. Because a function is written with the assumption that a certain type of data will be passed in. But somewhere else, in the far reaches of the code, that function is called with an unexpected set of arguments.

This is why I contend that:

Type certainty is critical at the interfaces between programs.



Alt Text

Under the Hood

Once you get "under the hood" of the function - in other words, beyond the interface - the utility of "type certainty" quickly diminishes. As shown above, it's critical to know that the value of length is a positive integer.

So is it critical to know the data types of the variables inside the function? Not so much.

Ensuring the "safety" of the length variable is important because it emanates from outside the function. So, from the perspective of the function itself, it can never "know" exactly what's being passed into it. But once we're inside the function, it's easy to see (and control) the data types in play.

Inside createId(), we have the following variables:

id (string)
alphanumeric (Array<string>)
i (number)
randomNumber (number)
Enter fullscreen mode Exit fullscreen mode

Even if we converted this to TS, would it be worth our time to explicitly define all of these data types? Probably not. The TS compiler can easily infer the data types that are inherent in each variable, so it's unnecessarily verbose to explicitly spell them out. Additionally, any first-year dev can do the same just by reading the code.

More importantly, explicit data types inside this function will do almost nothing to minimize the creation of bugs. Because it's easy to grok all the data types at play, it's very unlikely that any flaws in the function's logic will be spawned by mismatched data types.

The only variable in the function that could really use some explicit "type safety" is the variable - length - that originated outside the function. That's the only variable that wasn't created explicitly inside this function. And that's the only variable that could create bugs that aren't readily apparent as we read this code.

This isn't meant to imply that there couldn't be other bugs lurking inside our function's code. But adding a pile of verbosity to define all the data types, for variables scoped inside this function, will do little to help us spot or fix such bugs. Because type-checking is not a magic bug-killing elixir. Type-checking is merely the first step in eradicating bugs.

This is why I contend that:

Type "certainty" has little value inside of a program.



Alt Text

Runtime FAIL

It may feel like I've just made a case in favor of TS. Even if you accept that type checking is most critical at interfaces, that's still a vital use of TS, right??

Well...

The real issue here is that TS fails at runtime. To be more accurate, TS doesn't even exist at runtime. When your app is actually doing its thing, it's nothing more than JS. So none of that warm, comforting type-checking means anything when your app is actually, you know... running.

This doesn't mean that TS is worthless. Far from it. TS excels when you're writing one part of your app that talks to another part of your app while exchanging your own trusted data. Where TS becomes borderline pointless is when your app needs to pass around data that was only defined at runtime.

When you're dealing with runtime data, if you want to create robust applications with minimal bugs, you still need to write all those pesky runtime checks on your data. If you start writing enough of those runtime checks, you may find yourself eventually wondering why you're even bothering with TS in the first place.

Let's imagine that our createId() function is tied to a user-facing application, whereby the user can request an ID of variable length. Let's also imagine that we've converted our function to TS. So our function signature would probably look something like this:

const createId = (length: number = 32): string => {
Enter fullscreen mode Exit fullscreen mode

Mmm, mmm! Look at that tasty TS type checking! It sure does protect us from all those nasty bugs, right??

Well...

If length ultimately emanates from a runtime source, then that comforting :number annotation doesn't actually do anything for us. Because, at runtime, the annotation doesn't even exist. So then we'd have to add some additional runtime checking, like so:

const createId = (length: number = 32): string => {
  if (isNaN(length)) length = 32;
Enter fullscreen mode Exit fullscreen mode

And that approach... works. But if that doesn't look kinda duplicative to you, then you've probably been writing TS code for too long.

In the function signature, it looks to the naked eye like we've defined length as type number and we've given it a default value of 32. But then, in the very first line of that same function, we're running a check to ensure that length is indeed a number. And if it's not, we're giving it a default value of 32.

Huh??

If you weren't already drunk on that sweet, sweet TS Kool-Aid, you'd be forgiven for wondering why we'd even bother defining a type number in the function signature at all. Of course, the answer is that, at runtime, there is no type declaration for length. So we end up checking its type twice. Once in the compiler, and once at runtime. Yuck.


Alt Text

Fuzzy Definitions

You may have noticed another problem with the data type definition above. We're annotating that length is of type number. But the definition of "number" is too broad - too fuzzy - to be of much use in our function.

We've already established that, for our function to properly generate IDs, length must be:

  1. A number
  2. Preferably, an integer
  3. Specifically, a positive integer


Any negative value for length is no more useful than passing in 'monkey'. 0 is similarly useless. Technically speaking, decimal/float values would work, as long as they're greater-than-or-equal-to 1, but they would imply a level of precision that is not accommodated in the logic. That's why it makes most sense to limit the input to positive integers.

This isn't a fault of TS. TS is built on top of JS. And JS's native types are... limited.

And even if TS had a custom type that allowed us to annotate that length must be a positive integer, we'd still be limited by the fact that those types are only available at compile time. In other words, we'd still find ourselves writing runtime validations for things that we thought we'd already defined in our code.


Alt Text

A Better Way

So is this just a "TypeScript Is Da Sux" post?? Not exactly.

First, I understand that there are many practical reasons why teams choose TS. And most of those reasons haven't even been addressed in this post. Many of them have little to do with the code itself. And that's fine. I get it.

For those teams, I'm certain that I've written absolutely nothing here that will change your commitment to TS - in any way.

Second, I've noticed amongst the "TS crowd" that there's this kinda mindless mantra about it. A persistent chant about all the supposed bugs they feel they've avoided in their glorious TS code. But the more I look at TS code - and the more I look at the way TS shops operate - the harder it is for me to see any quantifiable benefits. IMHO, the "benefits" are mostly in their heads.

For a certain type of dev, TS seems to provide some kind of comforting blanket. A mental safety net, if you will. It doesn't matter if you prove that the safety net is flawed and will break under minimal stress. Some people just get a "warm fuzzy" when they look in their code and they see all those comforting type definitions.

(And please don't go quoting any of that AirBnB-study nonsense. It was based on a wholesale refactoring of a codebase. Of course they eliminated a ton of bugs when they refactored to TS. That's the whole point of refactoring. They would've eliminated piles of bugs even if they refactored everything in plain ol' JS.)

Third, I'm not claiming that the answer is to simply throw out any notions of type "safety" or type validations. Far from it. In fact, I'm rather anal retentive about crafting fastidious validations - in all my functions - with nothing more than JS.

Back in March, I posted an article detailing how I do data validations - in regular JavaScript. (If you're interested, you can read it here: https://dev.to/bytebodger/javascript-type-checking-without-typescript-21aa)

My recent foray into TS has led me to revisit my JS type-checking library. And I'm happy to report that I've made some significant improvements to it. So significant, in fact, that I just don't see any reason at all to use TS in my personal development.

The next article I write will be a detailed illustration of how I use my new-and-improved JavaScript, runtime, type-checking library.

Stay tuned...

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player