17

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?"

Function overloading looks like this today:

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 OOP. 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?

2
  • 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.) Commented Nov 17, 2018 at 12:12
  • @underscore_d, I didn't find special resource for this curious question. However, I finally have found the answer below. Commented Nov 18, 2018 at 7:53

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

8 Comments

Brilliant answer! They should add this question and this answer to FAQ github.com/Microsoft/TypeScript/wiki/FAQ Now it is clear for me
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
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.
I see the difference; yes, that would be another way to do it. But that would still violate TypeScript's Design Non-Goal #5 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.
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.
|
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".

1 Comment

Thanks for the answer! BTW, there were a lot of decilned suggestions for adding function overloading in TypeScript repository, for instance [link] (github.com/Microsoft/TypeScript/issues/3442)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.