14

There are a lot of questions about how function overloading works in Typescript, (for instance, TypeScript function overloading). But there are no questions like 'why does it work in that way?' Now function overloading looks like that:

function foo(param1: number): void; 
function foo(param1: number, param2: string): void;

function foo(...args: any[]): void {
  if (args.length === 1 && typeof args[0] === 'number') {
    // implementation 1
  } else if (args.length === 2 && typeof args[0] === 'number' && typeof args[1] === 'string') {
    // implementation 2
  } else {
    // error: unknown signature
  }
}

I mean, Typescript was created to make programmer's life easier by adding some so-called 'syntactic sugar' which gives advantages of OOD. So why can't Typescript do this annoying stuff instead of programmer? For example, it may looks like:

function foo(param1: number): void { 
  // implementation 1 
}; 
function foo(param1: number, param2: string): void {
  // implementation 2 
};
foo(someNumber); // result 1
foo(someNumber, someString); // result 2
foo(someNumber, someNumber); // ts compiler error

And this Typescript code would be transpiled to the following Javascript code:

function foo_1(param1) { 
  // implementation 1 
};
function foo_2(param1, param2) { 
  // implementation 2 
}; 
function foo(args) {
  if (args.length === 1 && typeof args[0] === 'number') {
    foo_1(args);
  } else if (args.length === 2 && typeof args[0] === 'number' && typeof args[1] === 'string') {
    foo_2(args);
  } else {
    throw new Error('Invalid signature');
  }
};

And I didn't find any reason, why Typescript does not work like this. Any ideas?

Inflight
  • 497
  • 4
  • 12
  • 1
    "_But there are no questions like 'why does it work in that way?'_" AFAICS, that might be because "why" questions tend to quickly descend into speculation and opinion, unless (and sometimes even after) someone finds a discrete quote from a language architect. (Even excluding that, I don't know if SO is really designed towards "why" questions, but I might be wrong there.) – underscore_d Nov 17 '18 at 12:12
  • @underscore_d, I didn't find special resource for this curious question. However, I finally have found the answer below. – Inflight Nov 18 '18 at 07:53

2 Answers2

11

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!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 1
    Brilliant answer! They should add this question and this answer to FAQ https://github.com/Microsoft/TypeScript/wiki/FAQ Now it is clear for me – Inflight Nov 18 '18 at 08:09
  • 1
    I don't understand your point about type-checking at run-time. The compiler could *easily* convert each overload to a unique function at compile time, and replace the usages within code according to the signature. Similar to converting `foo(signature_1)`, `foo(signature_2)` to `foo_1`, and `foo_2` – Tobiq Apr 23 '19 at 18:53
  • The main difference is, instead of type checking at run time, like the OP suggested, you simply replace the usage, according to the signature, at compile time, so there's no central definition actually being used / overloaded at run-time. I've actually resorted to something of the sort, with a `fooType` function structure, as this is more succinct, efficient and simply more sensible than type guarding within the function. – Tobiq Apr 23 '19 at 19:02
  • I see the difference; yes, that would be another way to do it. But that would still violate TypeScript's [Design Non-Goal #5](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals) about emitting different code based on the results of the type system. So you're left with figuring out which overload to use at runtime, which would possibly involve an answer like this one. – jcalz Apr 23 '19 at 19:10
  • I don't really think the implementation of overloading would be any different to traditional polyfilling. Yes, it would require some thought about realising which overload to use at run-time, but there's still a logical 1:1 mapping between the functions at compile and run-time. The code is compiled and hardly needs to be accessible. – Tobiq Apr 30 '19 at 20:22
  • I think the solution the OP, and you spoke about would break the 5th non-goal regarding types at runtime, but this solution wouldn't. They're just 2 (or more) separate functions. – Tobiq Apr 30 '19 at 20:24
  • https://github.com/Microsoft/TypeScript/issues/3442#issuecomment-346950944 – Tobiq Apr 30 '19 at 20:33
  • If you are emitting different function names to JS depending on the types of the parameters passed into the overloaded function in TS, then that’s not allowed. I agree it is technically feasible; it just runs counter to the stated design goals for the language. It’s pretty explicitly stated: no emitting different code based on the results of the type system. – jcalz Apr 30 '19 at 21:48
3

Typescript was created to make programmer's life easier by adding some so-called 'syntactic sugar' which gives advantages of OOD.

This is not in fact one of the TypeScript design goals. You can read the goals here. Support for separate implementations for function overloads would fall under the non-goal of "rely[ing] on run-time type information".

Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75
  • Thanks for the answer! BTW, there were a lot of decilned suggestions for adding function overloading in TypeScript repository, for instance [link] (https://github.com/Microsoft/TypeScript/issues/3442) – Inflight Nov 27 '18 at 09:47