The secret trap when using ANY in TypeScript
How one little type wreaks havoc on certain overloaded functions.
What is any
?
If you’re working with TypeScript, chances are you’ll work with the any
type. any
essentially turns off typechecking, and allows the corresponding variable to be used for anything. You can call any methods on an any
variable, and they’ll all return any
as well. It’s great when you can’t write types for everything in your codebase.
What are function overloads?
TypeScript has another neat feature called function overloads. Some JavaScript functions return different results based on the arguments you supply, and this can be represented in TypeScript by written multiple function types on top of each other. Only one function signature can match at a time. The matching overload is determined by the arguments you supply to the function. The first applicable overload will always be chosen.
Using overloading with arrays
Some functions want to transform an array in some way and return an array with the same length and slightly modified types. A good example of this is Promise.all
, which transforms an array of promises into a single promise that resolves with an array of values.
Using generic function definitions, you can infer the type of the array passed into Promise.all
. However, the resulting type will be an array without positional data.
TypeScript does let you infer the type of specific array items, but then you need to hardcode the length. Using overloading, you can have a couple of variations for different array lengths.
However, you can only write so many overloads. Eventually, you need to fallback to an array with an unknown length like above. TypeScript 4.4’s official type definitions for Promise.all
hardcodes arrays up to length 10, and fallback after that.
How any
creates problems with overloads
Remember that any
will match with any type, and that function overloads use the first applicable overload. These two facts cause problems when you pass a variable with type any
into a function with overloads.
The first overload is always used when you pass in a variable with type any
because it will be applicable to that signature. Even if there is a more generic signature later, the first overload is chosen. The overload ordering cannot be reversed, because the more generic signature will match every variable you pass in. As far as I know, you can’t write a signature that only matches if the variable is type any
, because matching with anything works in both directions.
In the case of Promise.all
, the first function overload signature is an array with a hardcoded length of 10. That can create confusing bugs like this, where any
is passed in and the resulting type becomes an array of exactly 10 unknown
s.
Ok @typescript world - why is awaiting a map of
any
an array of exactly 10unknown
s? pic.twitter.com/GeEIVqwjvD— Evan™ (@EvanTahler)
October 12, 2020
Future solutions
Hardcoding the array length isn’t great, because you can only support so many variations. TypeScript 4.0 introduces a new feature called “variadic tuple types”, which lets you capture the exact array argument and transform it. TypeScript 4.5’s type definitions for Promise.all
replaces all the function overloads with a single function signature, removing the any
bug entirely.
TypeScript may also add special handling for passing any
into function overloads in the future. If you know of an existing issue, or see something I missed, let me know.