Blog

TypeScript to Make JavaScript Development Better and Faster

On a recent project involving AWS Lambda and AWS CDK, I made the decision to write the Lambdas in TypeScript to match the CDK code and found that it accelerated my progress noticeably. Later I realized that the back-end development team in that company had no TypeScript experience. I then had to explain the benefits of TypeScript over “plain” JavaScript. This blog post stems from and extends that conversation.

Why TypeScript?

There’s a lot of information about TypeScript on the Internet today, and I don’t intend this article to be a language overview. However, I’d like to list the reasons and give some examples of why I found that TypeScript improves the JavaScript development experience.

First, a few facts about TypeScript that you might not know:

  1. TypeScript compiles to JavaScript, so it’s compatible with browsers and Node alike, along with anything else that uses JavaScript. If you’re using Node, you could instead use Deno to run JavaScript or TypeScript (mixed, even) without compilation and a few other benefits. If you’re using Node, you can use ts-node with npx to run commands written in TypeScript with just-in-time (JIT) compilation.
  2. All JavaScript is valid TypeScript.
  3. TypeScript compilation also implicitly performs static-analysis.
  4. TypeScript compiles to a configurable JavaScript standard, so modern JavaScript features can be exposed and used in TypeScript, and polyfilled at compilation. Once the target interpreter (Node, web browsers, etc.) support those features natively, the same TypeScript code can then be recompiled without the polyfill.
  5. TypeScript adds strong typing to JavaScript.
  6. TypeScript also has a top-notch language server, and IDEs like Visual Studio Code (VSCode) that utilize language servers will have as-you-type static analysis.

What these add up to is a highly intelligent IDE experience that prevents you from doing something silly in your code that will likely result in bugs, many of which would be very hard to explain.

Dealing with Type Mismatches

Suppose there’s a dataset stored (or retrieved, at least) in JSON that includes an object with a key that could be legitimately missing. Let’s keep the example simple: a data structure of a person’s name, with the keys first, middle and last. Some people don’t have middle names. (Some people don’t have last names, etc. We’ll ignore those edge cases for now.) So, these two objects in this array are valid:

let idiots = [ { "first": "Arthur", "middle": "Philip", "last": "Dent" }, { "first": "Ford", "last": "Prefect" } ];

This is a simple data structure, and anyone who has coded in JavaScript for very long can tell you that the missing middle name can cause a ton of issues. But before we talk about how those issues play out, let’s go ahead and make it worse, since that’s how requirements often work.

Let’s say that this dataset also records a possible middle initial for the person, and we want to use the middle key as an object with keys name and initial when we do. An example:

let president_33 = { "first": "Harry", "middle": { "name": "S.", "initial": "S" }, "last": "Truman" };

To recap, we have first which is required, middle which is not required and maybe a string or an object with the subkeys name and initial (both optional string values), and then a required last key with a string value. In TypeScript, we would then define this type and use it thus:

type NameDescription = { first: string; middle?: string | { name?: string; initial?: string }; last: string; }; let ns: NameDescription[] = [ { "first": "Arthur", "middle": "Philip", "last": "Dent" }, { "first": "Ford", "last": "Prefect" }, { "first": "Harry", "middle": { "name": "S.", "initial": "S" }, "last": "Truman" }, ];

Notes:

  • The brackets ([]) after a type name means it’s an array (doc link).
  • A question mark (?) after a property name means it’s optional (doc link).
  • A pipe (|) between type names means it’s forming a union (doc link).

If you try to create a NameDescription data structure without a last or middle name, this is what you see in VSCode with the TypeScript plugin installed:

Property ‘last’ is missing in type ‘{ first: string; }’ but required in type ‘NameDescription’.

As you can see, that error is caught immediately, and compilation (which we didn’t even get to yet) would fail with the same code.

But even more powerful is what happens when logic is applied to those data structures. Let’s contrive a little further and say we have to store the names from NameDescription data structures into less complex structures in another database, and we need three required strings: F, M, and L. If the middle name is available, use that, otherwise use the initial, or use "<N/A>", in that order (assuming some bizarre legal requirement or such).

We could try to code it up as follows:

for (const name of names) { let new_name: LegalName = { F: name.first, M: name.middle, // <-- error on M L: name.last }; legal_names.push(new_name); }

As you see, we find a potential bug immediately. This seems trivial, but is fairly common when one developer coded the NameDescription type and another (or the same one, weeks later) coded the LegalName type. This saves looking up all of the possible permutations of the two types throughout the code. Here it’s clearly codified, assuming that the NameDescription type is written to correctly model the data.

Let’s take another swing at that, and see how it fares. Demonstrated with a screen recording (complete with typos along the way) so you can see the autocomplete, error catching, and type narrowing happen live:

Screen recording of live coding a solution in VSCode (no audio).

Shown in the video:

  • The TypeScript language server provides intelligent interaction with the code, giving useful auto-completion suggestions.
  • As-you-type error checking, so you can catch typos quickly. (The level of responsiveness shown requires Auto Save to be enabled.)
  • Type-narrowing happens in if statements. Checking if something exists eliminates undefined and null from the union type, for example. Checking that the typeof is "object" will narrow types down to objects and arrays. (To detect arrays use Array.isArray()).
  • You can mouseover to see the type computed of any variable to help diagnose issues.

Final Notes

One major downside of this is that JavaScript code will not have type information, and types will default to any, which effectively disables all of the type checking. However, a .d.ts “TypeScript Types” file can be provided (and more often is) with node modules to add type information to “plain” Javascript. TypeScript-based modules will compile to JavaScript and provide a .d.ts file as well. For many popular modules that don’t yet include TypeScript Types, there are @types/* npm modules provided by DefinitelyTyped, which also includes the Node built-in modules.

It’s important to resist the urge to use any just to get rid of the warnings. When you do that, you throw away all of the static analysis value provided by TypeScript. Of course, sometimes any is necessary, but use it sparingly and with hesitation.

TypeScript coupled with IDE support such as VSCode’s built-in support (utilizing tsserver) gets you as-you-type static analysis, code-completion, and mouse-over type introspection. This adds up to catching a lot of potential bugs, and the resulting (compiled) code runs anywhere JavaScript runs.

Categories: Blog

Rob Giseburt
04 May, 2021