You Might Not Need TypeScript (or Static Types)

Krispy Kreme — Scott Ableman (CC BY-NC-ND 2.0)

TypeScript has gained a lot of popularity since the Angular 2 project decided to adopt it and write all their documentation examples in TypeScript, but is it really worth the investment?

Before we get into this, I’ll preface it by saying that I’m a fan of the tools that static types enable, and TypeScript is currently my favorite static type system for the JavaScript community.

I come from a background using statically typed languages including C/C++ and Java. JavaScript’s dynamic types were hard to adjust to at first, but once I got used to them, it was like coming out of a long, dark tunnel and into the light. There’s a lot to love about static types, but there’s a lot to love about dynamic types, too.

I’m not religious about tech stacks and dev tools. I’m practical. I do consulting, and a lot of dev teams use Angular 2 and TypeScript these days. If I’m going to give them advice, I’d better know what I’m talking about.

I strongly recommend that you adopt a similar open-minded attitude towards new and different tech stacks and tools. Believe me, you’ll learn more than a few during your career.

Being a developer means that you’re choosing a lifetime of learning new things. It’s a good idea to make peace with that right now. That doesn’t mean learn all the new things, but do be open to new things if you need to learn them for your work, or just the joy of discovery.

The joy of discovery is one of the best things about being a software developer.

A few months ago, I decided to kick the tires of both Angular 2 and TypeScript. I went all-in full-time for months, contributing to a real production app on a real dev team.

My general conclusions went something like this:

  1. Angular 2 has lots of stuff that’s not in React, but all those extra things didn’t make me more productive, and most of the good stuff is available in 3rd party modules.
  2. TypeScript got in the way more than it helped. It didn’t reduce bugs, and didn’t enhance our productivity, either.

You can read more about that adventure in “Angular 2 vs React: The Ultimate Dance Off”.

If you’re already a TypeScript fan, you’re probably already itching to comment on this post. Before you do, please read “The Shocking Secret About Static Types”.

The gist: Static types don’t reduce over-all bug density by much, but you may still love them for the developer tooling.

Everybody’s always talking about all the benefits of static types, but relatively few articles go into the drawbacks. Let’s talk about those a bit.

How Maintenance is Hindered by Static Types

Type annotations obviously create more syntax noise, and that syntax makes code harder to read, and harder to maintain. But the drawbacks go a little deeper. Specifically, static types can make all of these things harder (not impossible, just more complicated than they need to be):

  • Generic functions & polymorphism
  • Higher order functions
  • Object composition

That’s a drag, because I use all of those things a lot, and if you’re a fluent JavaScript coder, chances are good that you use them a lot, too.

I don’t want to turn this post into a book, so we’ll focus on my biggest pet peeve about static types: generic functions.

A generic function is a function that can operate on parameters of more than one type. For example, a function that calculates sums might work for numbers, but could also work with objects which supply a .valueOf() method that returns a number (note, in JS, numbers have a .valueOf() method, which makes it possible to treat both types using identical internal logic).

In a language like C++ or Java, generic functions require type constructors or templates, which take type parameters in order to achieve compile time function polymorphism.

The syntax you need to parse then becomes multi-dimensional because the single function is really two functions: The parameterized type function and the concrete function which actually performs the behavior.

In a dynamically typed language, there’s no need for type constructors. Resolving the types is abstracted away from the developer. It still happens, but it happens under the hood at runtime, and the developer doesn’t need to think about it.

The effect is that developers don’t get distracted by type constructors or template syntax. Instead, developers can use duck typing, and optionally perform runtime type checks.

Dynamic Types Don’t Mean No Type Checks

Runtime type checks are type checks that happen at program runtime, at the moment when the type is actually used. Dynamic type checking is built into JavaScript, but it’s very relaxed, and only throws errors when you try to do something super crazy like invoke undefined as a function.

When that happens, your software may crash. Possibly bringing down your server in production. Oops.

To get stricter type checking, you can define your parameter types and then wrap your function with a type checking function.

For example, with JavaScript + React, we can use React’s PropTypes for automated dynamic type checks in development mode, and even compile the type checks away for production deployment, since they’re no longer needed once you’ve verified that the correct types are being passed.

In other words, the application doesn’t need to take a performance hit to benefit from runtime type checking.

Here’s a stripped-down example of how you might enhance runtime type checking in JavaScript:

A more clever version could parse the text of the function signature and extract type requirements from default parameter values. For example, if you tried to pass a Date object instead of epoch time for the hireDate, you could throw a type error.

React’s PropTypes aren’t handled with a wrapper function like this. Since React is a framework, it can arbitrarily and conditionally insert type checks anywhere during the component lifecycle. Other frameworks can accomplish a similar distinction between development and production behavior:

If you’re thinking this sounds like a lot of trouble compared to static types, consider that runtime type checking complexity is usually hidden inside a framework or library. The creator of the app above only needs to worry about the createEmployee() function and its required property. In other words, it can be less work than writing the equivalent static type annotations.

Note: There are a handful of open source libraries designed to do what I’ve just described. If you’d like to contribute to an experimental one that can be easily used in any JS program with or without React, check out rfx.

Will Type Errors Bring Down My App?

Type correctness does not guarantee program correctness.

Type errors are not the only source of bugs, and throwing an exception is not the worst thing that can happen in response to a bug.

Hopefully you’ve taken my advice on using TDD. Static types are great at catching type errors, but have little effect on over-all bug density, TDD can reduce production bug density by 40% — 80%. Code review is also a very effective means of reducing bug density. Each hour spent on code review saves 33 hours of maintenance[1].

If you use both TDD and code reviews, very few type errors will escape into production.

Add runtime type checks to the mix, and you’ve got a 3-layered line of defense against shipping bugs. I’ve seen these strategies used to ship million+ LOC code bases with very low bug density for production apps serving tens of millions of users.

Anyone who tells you that you need static types to ship large, complex apps is full of hot air. A high-quality continuous delivery approach has a much larger impact on bug density and project success than the presence or absence of static types.

What is duck typing?

Duck typing is a method of type checking that looks at the structure of a value rather than its name or class. It’s like feature detection for objects. If it walks like a duck and talks like a duck, we treat it like a duck, even if it’s not.

Systems like React’s PropTypes in JS are structural type checkers, which means that the type is checked based on its shape (the names and types of its properties) rather than the name or identity of a given type.

A type error is only raised if the required features are not present. Duck typing is beneficial because at design time, you don’t have any way to know all of the future requirements of the program. What if in the future you need a rubber duck drone which can still fly and quack like a regular duck?

With nominal static types, you’ll need to change the type signature of every function that uses ducks everywhere in your program — a process that is not easily aided by automated refactoring tools because the functions that need changing don’t yet know that rubber duck drones exist.

With duck types and certain structural type systems, you don’t need to change signatures to accommodate rubber duck drones.

Recap

In the case of generic functions, maintenance is hindered by static types in two ways:

  1. The added complexity of templates and type constructors makes it harder to design and understand generic functions in statically typed languages.
  2. The restrictions of nominal static types make it harder to add capabilities to the program in the future.

The second point does not apply to structural type systems such as TypeScript, which also checks for feature availability as opposed to name or identity checks. It does apply to nominal type systems, including Java and C++.

Generic Functions in TypeScript

TypeScript does suffer from added complexity for generics. Take a look at this generic identity function in standard JavaScript:

const identity = arg => arg;

Compare that to the much noisier statically typed function in TypeScript:

function identity<T>(arg: T): T {
return arg;
}

Neither of these functions actually needs any type information, because the value is passed through without using any of the parameter’s features.

Dynamic type systems like the one built into JavaScript can optionally use static data flow analysis to infer and track the types of arguments that pass through the standard JavaScript version of the function. That’s how systems like Tern.js and Facebook’s Flow work their magic. (Note: Flow is a static type system with dynamic inference & data flow analysis capabilities… annotations optional).

For example, Flow doesn’t need a type constructor to keep track of identity’s types. Edit: TypeScript 2 has added similar capabilities.

const identity = arg => arg;
const num:number = identity('NaN LOL');
// "String. This type is incompatible with number"

Type Inference Rocks

In ES6, functions can specify default values, which can be used for type inference by compatible type inference systems such as Tern.js, TypeScript, and Flow. For example:

const createEmployee = ({
name = 'Unnamed Employee',
hireDate = Date.now(),
title = 'Worker Drone'
}) => ({
name,
hireDate,
title
});

Here’s the signature type hint displayed by atom-ternjs:

In other words, you can gain 99% of the benefits of a static type system using nothing but standard, dynamically-typed JavaScript code paired with an inference-capable IDE tool.

One great thing about TypeScript is that when the inference capabilities inevitably get a signature wrong (which frequently happens when a function wraps another function), TypeScript allows you to manually assign an interface to something so that correct type hints get displayed in your editor.

I wish there was an optional way to do that built into JavaScript.

What About Automated Refactoring?

Two points:

  1. Any dev tool that can grok a dependency tree and has type inference and data flow analysis can do most of the same automated refactors you’ve come to expect from static type tools. e.g., Tern.js.
  2. In multiple decades of software development, I can count on one hand the number of times I’ve spent more than a few minutes manually refactoring something in a way that can be substantially assisted by static types.

What About Autocomplete?

Every decent editor has good autocomplete plugins available. Many support type inference with assistance from Tern.js, Flow, etc… Here’s Atom’s built-in autocomplete-plus in action:

Atom’s autocomplete-plus

What About Identifier Name Typos?

Any decent linter can catch those errors. Here’s ESLint in action:

ESLint in action

Conclusion

It seems to me that a lot of static type advocates are unaware of the capabilities of modern dynamic type tools, such as runtime type checking, and type inference with data flow analysis.

If you can get 99% of the benefits of static types without the extra syntax noise and cognitive overhead of type annotations, will static types really give you a net win in developer productivity?

Are static types really worth the trade-offs?

In my experience, the answer is yes and no.

Yes, because the developer tooling for TypeScript is currently better than the tooling for Tern.js and Flow (last I checked). By better, I mean that the UI is nicer. It’s easier to use. Type errors show up looking like the syntax errors from the linter, beautifully formatted. It’s super useful. I’m a little in love with it. You might fall in love, too.

In my opinion, TypeScript offers the best developer tooling experience available in the JavaScript world today.

If TypeScript’s tools would provide hints and type inference for standard JS files by default, I’d use it instead of Tern.js and recommend that setup to everybody. Easy choice.

Note: There’s no reason that Tern.js and Flow can’t match TypeScript’s developer tooling UX. Somebody just needs to invest some TLC in the editor/IDE plugins.

And No, because as TypeScript works today, you need to educate developers on your team about how to properly use TypeScript, and how to keep code as free of syntax noise and annotations as possible, while still providing enough type clues and annotations to make it worthwhile.

I’ve seen a production TypeScript project with over 1,000 type errors, absolutely littered with any annotations (the go-to escape hatch when static types get in the way). No matter how much you hear that TypeScript makes it easier to run big projects, big projects come with big teams, and developer education and buy-in is one of the hardest problems in software development.

TypeScript is definitely cool. I really like it. But it comes with a substantial cost, and it wouldn’t be wise to ignore it. Before you decide to use TypeScript, take a good hard look at your team and ask yourself very carefully and honestly:

  • Is your team ready for the learning curve?
  • Will you have the discipline to invest in training and mentorship to bring your developers up to speed on TypeScript?
  • Will you always be able to maintain that same training discipline while you onboard future hires?
  • Most importantly from an ROI perspective: Are the modest improvements offered by TypeScript really worth all that extra effort?

OR:

  • Is the 99% benefit you get from standard JS + Tern.js + ESLint + autocomplete + TDD + Code Review good enough for your team?
Tip: Even with static types, you still need linting, TDD, and Code review to substantially reduce production bug density.

Bottom line: There’s no question that static types can feel good. Biting into a hot glazed donut feels good. But is it really good for you?


  1. Russell, Glen W. “Experience with Inspection in Ultralarge-Scale Developments,” IEEE Software, Vol. 8, №1 (January 1991), pp. 25–31.

Want to step up your JavaScript game? If you’re not a member, you’re missing out.


Eric Elliott is the author of “Programming JavaScript Applications” (O’Reilly), and “Learn JavaScript with Eric Elliott”. He has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.

He spends most of his time in the San Francisco Bay Area with the most beautiful woman in the world.