The single biggest mistake you could make in TypeScript

The single biggest mistake you could make in TypeScript

And how to recover from it

There is one thing in TypeScript that will undermine all your work and sabotage every benefit you might get from TypeScript. It's a very simple, insidious tsconfig.json value, noImplicitAny: false. Once you have code with implicit any’s, it is very hard to fix. I have made this mistake in the past; I know how much it hurts.

Let’s quickly take a look at an example

function calculateLoginDestination(user, path) {
    if (user.validated) {
        return `main/${path}`;
    }
    return calculateValidationRequestPath(user, path);
}

This might look like a harmless little function, do we really need types? Well, in a way this function does have types for both its parameters and its return value. The type is any because no type was specified. What happens if I call the function like this?

const path = req.queryParams.path;
const user = await getUserBySessionToken(req.cookies.token);

const destinationUrl = calculateLoginDestination(user, path);

Given the context that this function is used, the types for calculateLoginDestination might actually be as follows:

type calculateLoginDestination = (user?: User, path?: string) => string;

Now the problem with the above code should be obvious — we don’t check the presence of values! If user is undefined or null, the function will throw an error.

But it gets worse! What if I am changing calculateValidationRequestPath and I accidentally change it to return an object instead of a string? Since the return type of calculateLoginDestination is implicitly any, typescript won’t warn me. Now destinationUrl is actually string | object! An entire codebase of poorly typed functions quickly becomes a nightmare to modify, whether refactoring or adding features.

Lets see what would happen if we had enabled the noImplicitAny rule from the beginning.

function calculateLoginDestination(user: User, path?: string): string {
    if (user.validated) {
        return path ? `main/{$path}` : `main/`;
    }
    return calculateValidationRequestPath(user, path);
}

And in the calling context

const path = req.queryParams.path;
const user = await getUserBySessionToken(req.cookies.token);

if (!user) {
    res.status(401).json({ error: “Invalid session” });
    return;
}

const destinationUrl = calculateLoginDestination(user, path);

Our code now properly guards against missing users, and changes that might affect this code. If I modify getUserBySessionToken such that it returns a result object either containing a user or an error message, TypeScript will no longer compile until I update the above code.

If I modify calculateValidationRequestPath such that it no longer returns a string, typescript will no longer compile until I either fix the function or modify the return type of calculateLoginDestination and its callers.

What if I already have a codebase with implicit any?

I’ve been there. You start a project with noImplicitAny turned off, and later regret it. How best to fix it? There is really only one approach.

  1. Turn on noImplicitAny and add explicit any throughout your entire code base!
  2. Have the engineers on your team add types until there are no more anys.

The first step has to be a Big Bang change. It is painful, but necessary. If your code base is so large no one can make a change that large in a single commit, then turn it on file by file. But it is paramount that you get this done in a single day.

The next step is eating the elephant. You have to do it in small chunks, piece by piece until it is done. As you add types, it has a positive contagion effect, where the types propagate throughout your codebase and the amount of typing increases over time. Hopefully you can get the team to swarm on it and add types until the entire codebase is converted in one day. If you can’t, at least with the noImplicitAny rule turned on your code won’t get much worse in the meantime.

Hopefully you never have to deal with implicit any's, or any's at all for that matter. These days, tsc --init has noImplicitAny turned on by default, so it shouldn't be an issue. And if you ever see a pull request disabling the noImplicitAny rule, do the world a favour and request changes.

Let me know in the comments if you have any implicit any horror stories of your own!