DEV Community

loading...
Cover image for How TypeScript squashed all known bugs

How TypeScript squashed all known bugs

integerman profile image Matt Eland ・3 min read

You know that application that keeps you up at night hoping you won't have to change it? It seems like we all have one.

Mine was in JavaScript. It was a single page application made with just JQuery and JavaScript and it was the most brittle thing I've ever worked with.

Let me tell you how switching to TypeScript fixed all known bugs with the application while preventing entire classes of defects from ever popping up again.


Finding Existing 'Linking' Issues

The first benefit we saw when cutting over this application was that it was immediately apparent where methods were being called with too many parameters or too little, or in the cases where method names were blatantly misspelled (this was a large and undocumented codebase without many experienced individuals working on it or standardized processes).

By forcing everything to go through tsc (the TypeScript compiler), methods that didn't exist stopped the program flow in its tracks

Stringly-typed Variables

We had a number of variables which were being compared against numbers in some places. That's okay-ish if you're using == comparison where JavaScript tries to convert between stringly-typed numbers and actual numbers, but if you even tried to use === (which you should - for safety and for performance), you'd quickly find that '1' === 1 would be false and you'd have a lot of errors.

By declaring all of our parameters and variables with explicit types (using : number for example), we were able to catch these issues as well.

There were a few stragglers - places where we declared the value as a number, for example, but it actually read in as a string from the user interface, but these weren't too hard to track down once we figured out what type of code to look for.

Catching silly mistakes with ESLint

We used ESLint to lint our TypeScript code since TSLint indicated at the time that it was being deprecated sometime in 2019.

ESLint allowed us to catch probable issues such as return values not being assigned to a variable or other correctness issues. Since we incorporated ESLint into our build process, this meant that any time we built we'd catch any new linting errors.

Additionally, we augmented ESLint with Prettier for code formatting, giving us a consistent code style without us having to worry about it too much.

Introducing Classes

Because we had the transpiler to catch any blatant issues and linting to catch any new goofs, we felt safe to start taking our loose JavaScript functions and move them into classes, bringing a lot more organization and standardization to our TypeScript and highlighting more opportunities for code reuse and consolidation.

Eliminating Global State

Because we introduced classes, we had to start moving state out of global scoping and into individual classes that were responsible for it.

It turned out that behind blatant method parameter mismatches and comparing values of different data types, that poor state management was the next largest cause of bugs in the application.

While we didn't have the time on this project to introduce a solution like Redux or other related frameworks, the very act of moving state into individual classes turned out to be good enough to give us the control we needed to find the improper state manipulation code and fix it.

Testability

Taking a large nest of spaghetti JavaScript and chopping it up into TypeScript classes also allowed us to write Jest tests around individual classes, giving us a further degree of safety and confidence while making changes to the application.


Ultimately, migrating an old legacy JavaScript application to TypeScript turned out to be a fantastic move for development, quality assurance, and the end users because the additional rigor and safety measures we introduced stabilized the application to the point where we could make changes without breaking things.

Sure, I didn't get to convert the application to Angular or add a state management framework like Redux, but at the end of the day, the project finished early and without defects and resolved a large number of existing defects in the process - to the point of it being faster to move the application to TypeScript than to hunt down each individual bug in JavaScript and fix it, hoping we caught all aspects of it.

Every project is different, but I strongly recommend you consider TypeScript if you need to bring order and quality to an aging application.

Discussion (28)

pic
Editor guide
Collapse
lampewebdev profile image
Michael "lampe" Lazarski

Besides the click bite title, I find the problems you're describing are not a thing that typescript itself is a solution too.

All of them are tooling and design problems.

  • Finding Existing 'Linking' Issues

    • This is solved by VSCode or Webstorm
    • You just have to use Import/Export's
  • Strongly-typed Variables

    • typeof is your friend here
    • Okay typeof has it strange behaviors
    • Also TypeScript also has its quirks here Promise<MyType<T>>. Is this more helpful maybe?
  • Catching silly mistakes with ESLint

    • Not related to Typescript
  • Introducing Classes

    • Not related to TypeScript
    • Putting everything into classes can also tend to code overhead and bad design and structure. Look at any more significant Java project
  • Eliminating Global State

    • Not related to TypeScript
    • Can be solved with ESlint
  • Testability

    • Not related to TypeScript
    • Testing is hard

In general, it sounds more like what the most significant difference here is that the team is more experienced and knows better what is needed. I'm not against TypeScript. It has its good things, but also it has its downsides. Like no abstract classes. Even OOP fanatics now say that composition is a better way then class inheritance (SOLID principle).

Collapse
integerman profile image
Matt Eland Author

You are correct in the experienced aspect. However, the point in question here is that given a specific JS file with thousands of lines with existing issues, you need a way of making sense of it. TypeScript was that way, and the conversion process was what closed a multitude of bugs.

You'd be surprised what WebStorm has trouble interpreting with enough global state and dynamic code.

So, if there was a more efficient way of removing all bugs from the file, I'd love to hear it, but I will not debate that this was the answer I chose, and that it was extremely effective and needed.

Collapse
lampewebdev profile image
Michael "lampe" Lazarski

I mean the first red flag for me here was: having a file with thousands of line. Again bad design or bad practice.

My point was again not saying that typescript itself is bad but it is not the holy grail.

Team communication, team standards and aiming for best practice are.

I'm not debating the answer here. For me it just looks like the wrong conclusion what helped is mad here.

But this is just my view and both are okay.

Thread Thread
256hz profile image
Abe Dolinger

Of course the ideal is to create a big project with best practices from the beginning. I think the post is about a time that didn't happen, and how TS was the shortest path to a more robust codebase. Not that it's a holy grail, but that it helps make sense out of spaghetti.

Thread Thread
lampewebdev profile image
Michael "lampe" Lazarski

Yeah exactly! The goal was: clean up the code and create best practices at least this is what I get from that post!

My argument was: As far as I can see for the reasons mentioned here in the post is not fully typescript but also eslint and the most important thing: the team! The team now has a better base to do great work! They are now more experienced and know their domain better! I can guarantee if they would rewrite it in plain javascript with eslint they would be as good as they are now with typescript. The only thing they have no bigger job security because 2 3 years from now we as a community will agree that typescript is not the thing we needed. Like it happened before with CoffeeScript and flow.

But maybe I'm completely wrong.

Collapse
adamant127 profile image
Adam Jones

Typescript adds expressive compile time type info. typeof doesn't compare. I think the main point was that the extra safety from types made the other refactoring possible. That's not surprising since types are basically a kind of formal proof of program code.

Collapse
blindfish3 profile image
Ben Calder

To say typeof doesn't compare is absurd. Compile-time type-checking definitely has its uses; but people seem to forget that (unless I am very much mistaken) no type checking is being carried by the compiled code at run time. In which case you absolutely must do type-checking on data coming from sources outside your application. Typescript is not a replacement for proper data validation for which typeof is pretty much essential.

Thread Thread
integerman profile image
Matt Eland Author

You are correct - TypeScript's checks are entirely at the transpilation level. You do occasionally need to do some checking at some boundaries, but things inside of your systems can generally be trusted to be properly typed if you have proper validation at the boundaries and rely on tsc to catch the other issues.

Thread Thread
blindfish3 profile image
Ben Calder

I am not mistaken. From Typescript design goals:

Non-goals

  • ...
  • Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.
Thread Thread
integerman profile image
Matt Eland Author

I feel like you're really wanting to talk.

Thread Thread
blindfish3 profile image
Ben Calder • Edited

Is that a problem?

The nesting on comments maybe isn't clear - but I think only one of my comments was directed specifically to you ;)

edit: And just to be clear: I thought your article was interesting; but the focus on TS as the solution to the problems you described could be inferred by some to mean that JS was the cause of those problems.

Collapse
lampewebdev profile image
Michael "lampe" Lazarski

I hear this from the first time I ever coded and still, I have never seen a study/scientific proof of that being true. Most of the time it is just gut feeling.

The example in the code was about how == and === works in JS and that '1' === 1 is not good.

In all of that discussion what people tend to forget: In the end, it is Javascript that runs.

Collapse
pavelloz profile image
Paweł Kowalski • Edited

Yep.
I am against TS (i think its more an obfuscator and a distraction than anything else) and tihs is exactly what i was expecting going into this link. That most likely it will be all fixed by other things, but TS brings clicks, so here we are.

Collapse
integerman profile image
Matt Eland Author

Thanks for the feedback. I'll let product know we're reverting to last year's version of the code and going with a new approach.

This is a testimonial as to how a language helped me. Please keep hate out of it.

Collapse
itachiuchiha profile image
Itachi Uchiha • Edited

I still confused, for example, I create a method like this;

function aTypeScriptMethod(age: number) {

}

And backend data returns like that;

{
  name: 12,
  age: 'John'
}

This is wrong I know. But I just imagined.

I used this data like that;

let age = dataFromBackend.age
aTypeScriptMethod(age)

So, how can help us TypeScript in this case? I really don't have enough knowledge about TypeScript.

Collapse
olvnikon profile image
Vladimir

In the end, nobody replied to the question. You need to use a type guard for this. It is when static typing becomes dynamic. You have to manually check each field in the payload by typeof. Check the official docs.
typescriptlang.org/docs/handbook/a...

Collapse
wilgert profile image
Wilgert Velinga

TypeScript does not do anything in this case. TypeScript only checks types at compile time while the types of the response from the backend is known only at run time. There are some tools/libraries that allow you to use TypeScript types at runtime like: github.com/gcanti/io-ts.

Collapse
integerman profile image
Matt Eland Author

That's really interesting. I'm honestly not sure how I feel about that type of a library as it's not just a simple transpile down to JavaScript, but the injection of runtime type checking. I'll have to think more about that but I have a pretty strong gut negative for whatever reason. Thanks for sharing the library. I learned something new there.

Collapse
owenpattison profile image
Owen Pattison

So your aTypeScriptMethod would have a strict typing of number which means it will only accept that type.

This means when you later set age from dataFromBackend.age the let would be cast as a string.

Therefore when you try to pass the function a string you would see a compilation error, that would give you feedback saying you have tried to pass a string to the method rather than a number resulting in your code not compiling until you resolve the casting of age.

Collapse
blindfish3 profile image
Ben Calder • Edited

Owen - Either I'm misinterpreting your answer or it is simply wrong. Typescript only does compile-time type-checking. All it knows about data coming from outside is what hypothetical type you have assigned to your input parameters; but no run-time type checking or data-validation is embedded into the compiled code that actually handles the data: it has been compiled to plain JavaScript with no type checking.

In the example given Typescript is no help at all: the application would simply crash. I've seen this happen recently in an application taking data from a PHP backend out of our control. The data didn't conform to the data type we expected and our application bombed because someone forgot to add the proper checks. This was in QA so no big deal; but could have been really problematic if it had slipped through the net.

I think it would be unfair to call this a flaw in Typescript; but it is really important that people understand this. Typescript is not a substitute for doing proper type-checking/validation on data that is outside your control.

Thread Thread
integerman profile image
Matt Eland Author

If that was to me, you need to read it in context.

tsc doesn't generate type checking logic, it does static tranpilation analysis based on what you've told it about the types. If you tell it the Web API result returns a number, it'll roll with it and find no issues (assuming you treat it like a number). Since it transpiles down to JavaScript, the variable will still get assigned a string and you get the same sort of an issue down the road. That's why it's important to get your type definitions correct as well as potentially validate them at the boundaries with a typeof check.

Also bear in mind union types and nullability are supported in TypeScript and should be considered and used. The benefits once you understand them are great, however.

Thread Thread
blindfish3 profile image
Ben Calder

Matt - my response saying the answer given was incorrect was not directed at you. I've edited it to make that clearer ;)

Collapse
integerman profile image
Matt Eland Author

Put simply, typings tell the compiler what you think something is and it handles enforcement based on those assumptions. You think your incoming web service values are strings, so you define them that way and convert to number as needed when looking at the results.

Thread Thread
blindfish3 profile image
Ben Calder • Edited

Matt - I think you've misinterpreted the example problem: the string being passed in dataFromBackend.age can't be converted to a number; so your proposed solution could still cause a crash. The main problem lies here (my emphasis below):

You think your incoming web service values are strings

Actually you "think" your values are numbers masquerading as strings but you still don't have an absolute guarantee*; so you need to add some kind of guard in the code that converts to number to handle non-standard data. Without suitable handling your entire application could crash. As an extreme example I know of an OS map library that would crash your browser and require a reboot if you passed it a string instead of a number...

* except maybe if your backend is written in Typescript and shares the same type-definition

Collapse
mmakrzem profile image
Marek Krzeminski

Could you share your eslint rules

Collapse
integerman profile image
Matt Eland Author

I unfortunately cannot share any code with this particular article due to shared ownership. Was there some specific aspect you're curious about? I could talk about that in more depth.

Collapse
mmakrzem profile image
Marek Krzeminski

I've been using typescript for about 2 years now and my lint rules are defined in tslint. I'm looking to convert everything over to eslint, so I'm curious to see what rules people are using. I am aware of airbnb rules but I'm interested in seeing what other things people typically lint

Collapse
seangwright profile image
Sean G. Wright

Great overview of the benefits of a type system (and bringing good design patterns) with an in-use code base.