It's an interesting exercise to think about how you would implement "true" function overloads in TypeScript if you wanted to. It's easy enough to have the compiler take a bunch of separate functions and make a single function out of them. But at runtime, this single function would have to know which of the several underlying functions to call, based on the number and types of arguments. The number of arguments can definitely be determined at runtime, but the types of the arguments are completely erased, so there's no way to implement that, and you're stuck.
Sure, you could violate one of TypeScript's design goals (specifically non-goal #5 about adding runtime type information), but that's not going to happen. It might seem obvious that when you're checking for number
, you can output typeof xxx === 'number'
, but what would you output when checking for a user-defined interface
? One way to deal with this is to ask the developer to supply, for each function overload, a user-defined type guard which determines if the arguments are the right types. But now it's in the realm of making developers specify pairs-of-things for each function overload, which is more complicated than the current TypeScript overload concept.
For fun, let's see how close you can get to this yourself as a library which expects functions-and-type-guards to build an overloaded function. How about something like this (assuming TS 3.1 or above):
interface FunctionAndGuard<A extends any[]=any[], R=any, A2 extends any[]= A> {
function: (...args: A) => R,
argumentsGuard: (args: any[]) => args is A2
};
type AsAcceptableFunctionsAndGuards<F extends FunctionAndGuard[]> = { [K in keyof F]:
F[K] extends FunctionAndGuard<infer A, infer R, infer A2> ?
FunctionAndGuard<A2, R, A> : never
}
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type Lookup<T, K> = K extends keyof T ? T[K] : never;
type FunctionAndGuardsToOverload<F extends FunctionAndGuard[]> =
Lookup<UnionToIntersection<F[number]>, 'function'>;
function makeOverloads<F extends FunctionAndGuard[]>(
...functionsAndGuards: F & AsAcceptableFunctionsAndGuards<F>
): FunctionAndGuardsToOverload<F> {
return ((...args: any[]) =>
functionsAndGuards.find(fg => fg.argumentsGuard(args))!.function(...args)) as any;
}
The makeOverloads()
function takes a variable number of FunctionAndGuard
arguments, and returns a single overloaded function. And try it:
function foo_1(param1: number): void {
// implementation 1
};
function foo_2(param1: number, param2: string): void {
// implementation 2
};
const foo = makeOverloads({
function: foo_1,
argumentsGuard: (args: any[]): args is [number] =>
args.length === 1 && typeof args[0] === 'number'
}, {
function: foo_2,
argumentsGuard: (args: any[]): args is [number, string] =>
args.length === 2 && typeof args[0] === 'number' && typeof args[1] === 'string'
}
);
foo(1); // okay
foo(1, "two"); // okay
foo(1, 2); // error
It works. Yay?
To recap: it's not possible without some way at runtime to determine the types of arguments, which requires developer-specified type guarding in the general case. So you could either do overloading by asking developers for type guards for every overload, or by doing what they do now, by having a single implementation and multiple call signatures. The latter is simpler.
Hope that gives some insight. Good luck!