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.
- Turn on
noImplicitAny
and add explicitany
throughout your entire code base! - Have the engineers on your team add types until there are no more
any
s.
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!