Don’t let TypeScript slow you down

I’ve been using TypeScript for a few months and while I find it a must in any project beyond the most basic proof-of-concept, I did find myself being slowed down by it in some ways.

Naturally, the necessity of defining your interface would lead to more code, but I think there are quite a few methodologies that can be changed to improve our agility.

To emphasize and clarify: I ❤ TypeScript — This isn’t a hate article. These are merely some ideas that might improve your productivity with TypeScript.

Types for 3rd party libraries might not be worth it

Once we have intellisense, auto-complete and error checking on our own code, it is natural to want that awesomeness also for the 3rd party libraries we use.

Some libraries might have type information built in, for most, however, we need to use the external DefinitelyTyped repository. If it doesn’t exist, we can create our own (and even add it for the other to use).

That sounded very exciting to me initially. We’re going to add type to all the libraries! Everything in JavaScript will be strongly typed!

After spending way too much time on that, my suggestion is not to do that:

Complicated types code

Due to the dynamic nature of JavaScript many libraries include various dynamic return types and arguments. TypeScript is very powerful and you will most likely be able to express these types, but writing them and reading them will be challenging. For example, consider these types from LoDash to the flatMap function:

flatMap<T>(this: LoDashImplicitWrapper<List<Many<T>> | Dictionary<Many<T>> | NumericDictionary<Many<T>> | null | undefined>): LoDashImplicitWrapper<T[]>;

Due to this complexity when you do have type errors in your code, the error message will be just as complicated to understand.

Mismatch between types and library

When the library changes, your types would need to change as well. Since there is no direct link between the external types and the library, it might mean that the types from DefinintelyTyped you’re using right now, are wrong.

To clarify: If you’re using a library that natively supports TypeScript this shouldn’t concern you. If the library doesn’t, then just use it without types.

Dubious advantage

The greatest advantage of TypeScript is to allow us to validate types and prevent unexpected errors when refactoring. It is especially useful when working with complicated data structures.

Most javascript libraries don’t have complicated data structures and since we will not be refactoring the library itself the only advantage we’re really getting is argument/return-value type validation and auto-complete.

Auto-complete, while helpful, is really not necessary. Type validation, however, is important. We’ll see how we can have some even without complete library type information.

How to work with non-TypeScript libraries

So with that said, how do you work with 3rd party libraries?

The first thing to do, once you install a library is to try to import it as you usually do. If it works, it means that it has its own type definitions and you can proceed as normal.

If it doesn’t have type definitions, however, TypeScript will not allow you to use import. In that case, you must use the @ts-ignore syntax:

// @ts-ignore
import
_ from "lodash";

Now let’s use, for example, the same flatMap function I’ve mentioned previously. Here’s how I would write it to stay as type-safe as possible:

function duplicate(n: number) { return [n, n]; }
const data: number[] = [1,2];
const result: number[] = _.flatMap(data, duplicate);

Note how we set result and data to be of type number[]. Now even though we had no typing to lodash, the actual usage of the function is perfectly type-safe — If you ever try to assume that result is a string, TypeScript will complain.

We retained most of the advantages of TypeScript with very little headache involved.

Don’t be a type fanatic — Using “any” can be ok

All your code should have type information in TypeScript, especially your data models. However, sometimes you’re dealing with objects that can’t have an easy type.

It might be something you’re getting from your own legacy JavaScript code, a dynamic database value or a parameter that’s sent from a library. When a value or an argument is truly dynamic, creating super complicated generic types with various advanced TypeScript constructs might not be worth it. In these cases, using “any” is a legitimate option.

You should be using proper types as soon as you’re outside of the murky dynamic area and this should be especially avoided with your data models.

Remember that just like 100% unit test coverage is an unrealistic goal, the same can be said about TypeScript. Reaching that magic 100% type information will not necessarily make your code safer, but it is very likely to make it more complex and unreadable.

Prototyping is slower with TypeScript

One of the strengths of a dynamic language, like JavaScript, is the ability to prototype quickly, test things, break things and find the right way to do it.

We’d like to throw some random JSON on an API, see what it returns, adjust our code accordingly and play with various data transformations. For some things, we copy snippets from StackOverflow to see how they perform without code, for others, we’d like to use some new npm library and see if it helps.

TypeScript, on the other hand, likes us tread slowly. It wants every literal object to have a type and every function to have its argument defined. You can’t just write code as you please, and instead have to constantly stop and provide TypeScript type demands. It is especially jarring to do so only to discover that your idea doesn’t work and you have to delete everything.

So TypeScript is probably a bad idea when prototyping. We could always start a new dummy project just for our prototyping, but sometimes we want to prototype inside our current TypeScript project. What I would actually like to do is to disable TypeScript while I am prototyping, and then enable it later when I decided how to proceed.

The solution that worked for me was to disable TypeScript’s type checking in Webpack config:

{
loader: require.resolve("ts-loader"),
options: {
// this will disable any type checking
transpileOnly: true,
}
},

If you’re using ForkTsCheckerWebpackPlugin then you’ll need to comment it out as well. With these items disabled, TypeScript will compile your files as always, but will not create any errors on type issues.

Continue to prototype without distractions and once prototyping is done and you’re happy with the results, re-enable TypeScript and fix all the issues. Repeat the process every time you need to prototype something in your project.

TL;DR

To summarize my points:

  • TypeScript is really powerful and I highly recommend to use it. It is especially useful for your application data models.
  • Disable TypeScript while prototyping and re-enable once you’re done.
  • Don’t spend too much time trying to get type information to 3rd party libraries.
  • It is ok to use any for types that are too convulsed for a reason you can’t control.
  • As with code in general, strive for readability.