Function Overloads in TypeScript
Let's assume some JavaScript library defines a reverse
function that can work with both strings and arrays. In either case, it returns a reversed version of the input without mutating the original value:
function reverse(stringOrArray) {
return typeof stringOrArray === "string"
? stringOrArray.split("").reverse().join("")
: stringOrArray.slice().reverse();
}
Please note that this is a naive implementation only used for illustrative purposes. In a proper implementation, we'd have to deal with Unicode code points that are represented using two or more code units. We'd also do some more input validation. An even better idea would be to break up the function into two separate ones.
That said, how would we type the reverse
function in TypeScript?
#Version #1: Any Type
The simplest approach would be to annotate both the parameter and the return value with the any
type, for which any value in JavaScript is valid:
function reverse(stringOrArray: any): any {
// ...
}
Of course, with this approach the TypeScript compiler can't help us out a lot. Because we don't impose any restrictions on the parameter type, the compiler happily accepts parameters for which a runtime error will be thrown:
reverse(true);
reverse({});
reverse(Math.random);
We need to be a lot more specific than that to avoid mistakes like these.
#Version #2: Union Types
As a next step towards more refined types, we could use union types to specify that the stringOrArray
parameter must either be a string or an array of elements of an arbitrary type. The resulting union type is string | any[]
, which we use as both the parameter and return type:
function reverse(stringOrArray: string | any[]): string | any[] {
// ...
}
With these type annotations in place, the incorrect invocations from the previous example now result in a type error, while correct invocations are allowed:
reverse(true); // Error!
reverse({}); // Error!
reverse(Math.random); // Error!
const elpmaxe: string | any[] = reverse("example");
const numbers: string | any[] = reverse([1, 2, 3, 4, 5]);
Unfortunately, we've lost some type information. The type of the numbers
constant doesn't reflect that we passed an argument of type number[]
to the reverse
function. It would be more useful if the second constituent type of the union type was number[]
, not any[]
.
#Version #3: Union Types + Generics
A slightly better way to type the reverse
function would be to use generic types. Instead of typing the array elements as any
, we can generically type them as T
. That way, the stringOrArray
parameter is either of type string
or of type T[]
. The same goes for the return value:
function reverse<T>(stringOrArray: string | T[]): string | T[] {
// ...
}
Now, the type information is preserved:
const elpmaxe: string | string[] = reverse("example");
const numbers: string | number[] = reverse([1, 2, 3, 4, 5]);
Frankly, the function type is still suboptimal. Because of the return value's union type, we can't access array prototype methods such as map
, even though we know that we'll get back an array when we pass in an array. The type system, on the other hand, doesn't have that knowledge because we still haven't accurately modeled the possible parameter and return type combinations.
According to its signature, the reverse
function accepts a string or an array and then returns either a string or an array. Put differently, the function has the following four combinations of parameter and return types:
(stringOrArray: string) => string
(stringOrArray: string) => T[]
(stringOrArray: T[]) => string
(stringOrArray: T[]) => T[]
However, that's not how the reverse
function behaves. Only the following two combinations will ever occur at runtime, given the function implementation:
(stringOrArray: string) => string
(stringOrArray: T[]) => T[]
Let's see how we can reflect that knowledge in the type system.
#Version #4: Function Overloads
In other programming languages, we could overload the reverse
function by writing two functions with the same name, but different types:
function reverse(string: string): string {
return string.split("").reverse().join("");
}
function reverse<T>(array: T[]): T[] {
return array.slice().reverse();
}
That's not valid TypeScript, though, because we can't have two functions with the same name in the same scope. Think about this: How would the above code be transpiled to JavaScript? We'd end up with two reverse
functions that couldn't be distinguished by name.
Instead, TypeScript lets us specify an overload list to supply multiple types for the same function. That way, we can describe to the type system exactly what our function accepts and what it returns:
function reverse(string: string): string;
function reverse<T>(array: T[]): T[];
function reverse<T>(stringOrArray: string | T[]): string | T[] {
return typeof stringOrArray === "string"
? stringOrArray.split("").reverse().join("")
: stringOrArray.slice().reverse();
}
The first two lines of the above example list the valid overloads of the reverse
function. They represent the "external" signatures of the function, if you will. On the third line, we specify the generic "internal" signature, which must be compatible with all specified overloads. Here's how these overloads show up in an IDE (Visual Studio, in this case):
Notice how only the first two overloads appear in the autocompletion list. The implementation itself, which is typed using union types, doesn't show up. Also notice how we've been able to specify a nicer parameter name, depending on the type. And that's it! Using function overloads, we've managed to accurately type the reverse
function.