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.
let obj: any = { x: 0 };
// None of these lines of code are errors
const foo: any = obj.foo();
obj();
obj.bar = 100;
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 he function. The first applicable overload will always be chosen.
function convertType(value: string): number;
function convertType(value: number): string;
function convertType(value: string | number): number | string {
if (typeof value === 'number') {
return value.toString();
} else {
return parseFloat(value);
}
}
const num: number = convertType('number');
const str: string = convertType(num);
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.
class Promise {
static all<T>(array: (T | Promise<T>)[]): T[];
}
Promise.all([Promise.resolve(10), Promise.resolve('hello world')]).then(
(result) => {
// result's type is (number | string)[]
// @ts-expect-error: Type 'string' is not assignable to type 'number'.
const num: number = result[0];
}
);
TypeScript does let you infer 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.
class Promise {
static all<A, B, C>(
array: [A | Promise<A>, B | Promise<B>, C | Promise<C>]
): [A, B, C];
static all<A, B>(array: [A | Promise<A>, B | Promise<B>]): [A, B];
static all<A>(array: [A | Promise<A>]): [A];
// fallback to unknown length
static all<T>(array: (T | Promise<T>)[]): T[];
}
Promise.all([Promise.resolve(10), Promise.resolve('hello world')]).then(
(result) => {
// result's type is [number, string]
// This line is no longer an error
const num: number = result[0];
}
);
However, you can only write so many overloads. Eventually you need to fallback to an array with an unknown length like above. TypeScript’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.
function convertType(value: string): number;
function convertType(value: number): string;
function convertType(value: string | number): number | string {
if (typeof value === 'number') {
return value.toString();
} else {
return parseFloat(value);
}
}
const num: any = 10;
// @ts-expect-error: Type 'number' is not assignable to type 'string'.
const str: string = convertType(num);
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* 10 `unknown`s?22:31 PM - 12 Oct 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. Future type definitions for Promise.all
may replace 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.
Top comments (0)