TypeScript, web and the illusive type-safety-advantage

TypeScript makes us .NET developers comfortable with JavaScript. When you come from c#, coding JavaScript feels like walking on a tightrope without a safety net. With TypeScript comes a typesystem, a compiler and editors with intellisense. Also, the language syntax looks more familiar. This makes TypeScript popular amongst .NET developers doing web.

I’m torn. I like coding a TypeScript, but as an exception to the rule of .NET developers I actually also like coding in JavaScript.

The problem I have with TypeScript in web projects is that I often get the feeling, it was chosen because the developers prefer to avoid having to understand JavaScript and want to code the web-frontend in a language they are comfortable with.

Rather than trying to avoid properly learning JavaScript, I think anybody doing front end web should embrace it and learn to love it. Yes, really!

First and foremost, I feel that you should learn to use the right tool for the job, rather than trying to retrofit something you are comfortable with. But beside from my feeling of what’s right, I also think that you ought to consider the following: The browser executes only the resulting JavaScript and has not idea about the TypeScript behind it. So to make it work and especially to debug the resulting JavaScript you still have to understand all the details of JavaScript in a browser. You have not removed complexity, in fact you have added complexity, by adding another language, a compiler and an extra development workflow.

The following is an illustration of why using TypeScript can make your code more difficult to understand and debug, which is directly inspired (but simplified) by a real issue faced recently on my current project.

The calculator

The source

The following is the (trivial, I know) source code for a simple calculator written in TypeScript:

Pretty straightforward.

The unit test

Just to be sure, we’ll use jasmin to add unit testing:

The reason the last test is commented out is because TypeScript causes a compilation error:

So we have no type issues to worry about. As expected the tests pass. After all, there is a limit to how much we can screw up something this simple.

Skærmbillede 2016-05-06 kl. 10.55.51

The interface

So the code for the calculator works. Now all we have to do is use it in a web-interface:

In the html, we wire up input fields and result-label to use the amazing calculator-code.

This produces the following very simple interface and as one can see “divide” works as expected:

Skærmbillede 2016-05-06 kl. 11.03.43

We have used <input type=”number” /> in the markup and we have used TypeScript to write the calculator source code. We even unit tested the source doing the mathematical operations, so everything ought to work.

The problem

Nothing could be further from the truth. A quick press of the “Add” button reveals some strange behavior:

Skærmbillede 2016-05-06 kl. 11.11.18

We have managed to create a calculator, which can’t correctly add numbers. We used a compiled, typesafe programming language and even added unit tests to make sure everything works like it should. But it doesn’t.

The cause

To understand this issue, we have to look past the TypeScript code and understand the generated JavaScript:

The JavaScript doesn’t know, that arg1 and arg2 are supposed to be numbers, so they simply accept whatever

returns, which is a string regardless of type=”number” in the markup! And since the connection between the input fields and the calculator code takes place in JavaScript, the TypeScript compiler doesn’t help us at all.

wtfjs

The strange thing is then… Why is it that only “Add” fails, when the three other arithmetic operations work?

Well, this is because of JavaScripts typesystem, which one must understand to understand why our TypeScript code acts so strangely.

The ‘-‘, ‘*’ and ‘/’ operators in JavaScript only works with number types. So when we feed strings into those functions, JavaScript will attempt to convert those strings to numbers and then do the math. Like this:

Skærmbillede 2016-05-06 kl. 11.25.42

However, ‘+’ can also be used to concatenate strings:

Skærmbillede 2016-05-06 kl. 11.28.04

So when we pass to strings into the “add” function, it will concatenate the strings instead of adding the numbers.

The fact that we declared a TypeScript function that takes only numbers and returns a number

doesn’t help, because that the end of the day, the browser only knows about the JavaScript.

The solution

The solution is to stealth the “add” function by ensuring the input arguments are in fact converted to numbers by the use of the unary plus operator from JavaScript:

Shouldn’t be necessary, right? Also, since the TypeScript compiler prevents us from passing strings into the function, we can’t even unit test the function with string input to make sure the type conversion works like it should.

Other fun versions…

 

Another example – angular $stateparams

In another example I encountered, I thought I was clever, to define a TypeScript interface to ensure a correct declaration of $stateparams and help with renaming, refactoring etc:

and to go to the state I can use:

The problem here is, that even though I create a stateparams object with a number property, the constructor gets called with an object where all properties are strings. Because I used TypeScript, I thought the constructor received an object with certain types into the constructor, when in fact the underlying transport mechanism and JavaScript mechanics changed those types at runtime. And again, the compiler doesn’t help me at all, on the contrary it caused me to be very confused about why my code wasn’t working.

In conclusion

To sum up: using TypeScript in a web project, doesn’t mean that you don’t have to understand and deal with all the “weird” details of JavaScript. On the contrary, you will be faced with JavaScript weirdness in places, where you wouldn’t have imagined it possible.

15 Comments

  1. Mark

    So, statically-typed compilers can’t fix everything, especially interactions with things outside the statically-compiled language. No surprise there; I could give you similar examples from C# and WPF, MVC etc. The best solution I’ve found is to write code in TypeScript but tests in JavaScript.

    However, a quick tip for the StateParams case: you can specify a type for the parameters in your URL, as in ‘/foo/{id:int}’, which will make cause UI-Router to (a) only match on valid integers, and (b) set the stateParams property to a Number.

    • Anders Poulsen

      Thanks for the tip about StateParams. I wish I had known that… I’ll be updating my routes right away.

  2. BooYou

    Booo… Boo this man. Maybe we just like type safety. That has nothing to do with “a language we are comfortable with”. Everyone who understands TS knows that the types are thrown away at runtime.

    • Anders Poulsen

      I too like type safety. I just prefer to have either type safety or no type safety. In my opinion TypeScript is an in-between’er, that makes me feel like I have more type safety than I actually have, and then I get disapointed, when reality hits me.
      As for the “language we are comfortable with”, that is just my subjective impression, but it really is also backed by this msdn blog post: https://msdn.microsoft.com/en-us/magazine/jj883955.aspx

  3. Günter Zöchbauer

    That’s right, you still have to deal wirg JS weirsness. TypeScripts types are still a big improvement when your project grows beyond a handfull of source lines. Like Misko Hevery from the Angular project said: Now peoply finally are able to understand the Angular source code. If you want *proper* type safty in the browser you can also use Dart.

  4. Dandré

    Perhaps this is a gap for the TypeScript developers to fill. Perhaps what they could do for debug builds is generate JS that does “type checking” at runtime so that the developer using Typescript can understand that there is an impedence mismatch. Perhaps Typescript could also include certain “conversion” code to take care of this for you. I know this is a complex problem to solve in general but I wouldn’t say “dump Typescript” just because of a couple of edge cases.

    • Anders Poulsen

      I actually had sort of the same idea… I thought, that when I write function foo(bar:number) it would be nice if that was translated into function foo(bar:number){bar = +bar;
      Of course, this wouldn’t completely solve the problem, just introduce another kind of runtime error.

      • Sune Marcher

        IMHO, adding the type coersion + is NOT what you want the compiler to generate. The correct behavior would be testing if the input parameters have the expected types, and raise an error if not.

        The correct behavior is to do type conversion (and input validation!) when you grab values from your UI (or other relevant boundaries), and then have the rest of your code work on the correct static types.

  5. Enzi

    TypeScript can’t fix all problems, but it still provides a ton of value.
    I’m a .NET dev working on the backend of a huge project with a JavaScript frontend. It feels like not a day passes where I don’t see a “x is undefined” or “Cannot read property y of null” error swooping in on Raygun. We don’t use TypeScript, but if we’d do it all over again, I’d be strongly advocating it.

    The thing is, when a JavaScript codebase gets large, it becomes a very complicated game of Jenga. Every time you rename something or change the structure of an object, some other thing goes up in flames because it now accesses it incorrectly.
    In a statically typed world, this can’t happen, and with TypeScript at least the compiler will warn me.

    Even if TypeScript won’t prevent all the things from spontaneously catching fire, it will prevent lots of fires right during development. And I for one welcome any tool that helps reduce the hours I have to spend fire fighting.

  6. > The problem I have with TypeScript in web projects is that I often get the feeling, it was chosen because the developers prefer to avoid having to understand JavaScript and want to code the web-frontend in a language they are comfortable with.

    This. 100% agree.

    And that perspective is evident in the way TypeScript works and is designed. It’s not a very good solution to the “types for JavaScript” problem – especially when it comes to complex typing in functional scenarios, which is exactly what JavaScript needs. But it is a very good solution to the “I want my simple Pascal-style OOP so I don’t have to learn anything new” problem. Which is actually a valid problem, and a lot of teams choose TypeScript as a democratizing effect for the team instead of making them learn JavaScript.

    That said, I do use TypeScript because I love the IntelliSense and code completion features – they’re killer. But easily 20% of my time on every TS project is wrestling with the horrible typing system they have, and way too often I just have to “any” my way out because TS just isn’t capable of understanding my code. Too bad MS put all that work into TS instead of doing the same for JS.

  7. The real problem here is that your functions getValue() and setResult() were written in Javascript in the HTML. Had they been written in TypeScript and included in the file you call “the source”, the compiler would have alerted you to your use of a string as a number.

  8. There are other pitfalls with Typescript like confusing it’s classes with C# classes. This is why I refer to Typescript types as annotation rather than declarations. It’s a conversation with the IDE. It’s not about weirdness any more than any foreign language is weird because it’s different.

    In this case the problems would’ve been reduced if the GetValue/SetResult were in the .ts file rather than inline with the HTML. By hiding the information from the compiler you ar enot keeping your part of the bargain.

    • Anders Poulsen

      James and Bob, you are of course right about the solution you propose. It is partly due to the fact that I have tried to keep the example as simple as at all possible.
      The actual problem we faced involved a range slider in an angular app and looked somewhat like this:
      class ViewModel{
        public value: number;
      }

      <input type="range" ng-model="vm.value" />

      And again, yes the problem can be (and was) solved, but it wasn’t really obvious what was going on.

Trackbacks for this post

  1. The Morning Brew #2088 | Tech News
  2. Szumma #039 – 2016 19. hét | d/fuel

Leave a Reply