Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: Compile time function overloading #3442

Closed
zpdDG4gta8XKpMCd opened this issue Jun 9, 2015 · 35 comments
Closed

Suggestion: Compile time function overloading #3442

zpdDG4gta8XKpMCd opened this issue Jun 9, 2015 · 35 comments
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@zpdDG4gta8XKpMCd
Copy link

As we know JavaScript doesn't support overloaded functions, it's when a few functions with different signatures share the same name within the same scope where they are defined. Overloading can still be achieved at runtime by checking arguments and dispatching the execution to a proper path.

TypeScript seems capable of doing static function overloading which can be resolved at the compile time. Here is one way of how it can be done:

  • a notion of a nominal and effective name of a function is required
  • a nominal name is an identifier which is used to name a function and reference it in the TypeScript code
  • an effective name is an identifier which is used to name a function and reference it in the JavaScript code
  • an effective name is optional, if omitted then the effective name should be the same as the nominal name
  • a nominal name is a subject for overloading and hence can be shared across more than one function within the same scope
  • an effective name has to be unique withing the scope
  • the developer is in charge for specifying both nominal and effective names
  • the TypeScript emitter should only use the effective names for generating the JavaScript code
  • a resolution of a nominal name to an effective name should be based on the signature information
  • the hypothetical syntax (TBD) for a function declaration utilizing both the effective and nominal name might look like the following:
function nominalName:effectiveName() : void {
}

Example:

// typescript

function format:formatNumber(value: number) : string {
     return number.toFixed(2);
}

funciton format:formatDate(value: Date): string {
    return date.toIsoString();
}

var when = format(new Date());
var howMuch = format(999.99);
/// generated javascript

function formatNumber(value: number) : string {
     return number.toFixed(2);
}

funciton formatDate(value: Date): string {
    return date.toIsoString();
}

var when = formatNumber(new Date());
var howMuch = formatDate(999.99);
@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Jun 9, 2015
@jhchen
Copy link

jhchen commented Jun 10, 2015

Perhaps Typescript should just pick the effective name? A human would likely pick something similar anyways and part of the point of overloading is not having to think of a different name.

Given your example:

// typescript

function format(value: number): string {
     return number.toFixed(2);
}

function format(value: Date): string {
    return date.toIsoString();
}

var when = format(new Date());
var howMuch = format(999.99);

Typescript could keep track of unique function signatures and append an index in the generated Javascript:

function format1(value) {
     return value.toFixed(2);
}

function format2(value) {
    return value.toIsoString();
}

var when = format1(new Date());
var howMuch = format2(999.99);

Or it could pick more descriptive names by appending the type instead of the index:

function formatNumber(value) {
     return value.toFixed(2);
}

function formatDate(value) {
    return value.toIsoString();
}

var when = formatDate(new Date());
var howMuch = formatNumber(999.99);

A downside of option 2 is the names could be very long for some functions.

@zpdDG4gta8XKpMCd
Copy link
Author

@jhchen, although you are right that the compiler is able to generate synthetic effective names, i don't think it will contribute to the readability of the generated code, saving a little hassle with giving nams isn't worth less readable code

@wgebczyk
Copy link

If TS wants to be only transition to ES6+, then let's do what ES6+ spec says in that area.
If TS wants to be somethng more than that, this is very helpful as many other features that are available to the moment of generating JS.

Yes! I want language that has more features that JS(whatever), so please add as many as possible options that helps develop applications!

@jhchen
Copy link

jhchen commented Jun 11, 2015

Why have overloading at all if you still have to pick another name? The whole point of overloading is addressing the problem that the name the programmer wants is already being used. Requiring an effective and nominal name not only does not solve this problem, it introduces new syntax and unintuitive syntax purely for the benefit of generated code (which people read less and less given source maps). Also the generated names I suggested is very readable, especially considering generated code is most read during debugging and the prevalence of anonymous functions in Javascript. Sure it's less readable than a handpicked name by a human but this is precisely the burden that overloading is supposed to free us from.

In short, source code readability and developer productivity should be prioritized over generated code readability.

@zpdDG4gta8XKpMCd
Copy link
Author

@jhchen inconvenience of picking an extra name is a one-time thing whereas being able to address different functions by the same name is a repeatable positive experience

requiring an extra name does solve the problem

it's one of the primary goals of typescript to keep generated javascript readable

agree, the syntax might or might not be intuitive, subject for discussion

generated names that you have suggested do not account for multiple arguments whose types might be extra awkward and ugly

in short

  • the developer has to be in charge for giving names
  • inconvenience is mild and one time thing
  • primary goals of typescript are met

to kill both birds:

  • both nominal and effective name can be specified
  • effective name can be optional
  • if a nominal name is reused but effective name is omitted then a synthetic effective name is generated

@kitsonk
Copy link
Contributor

kitsonk commented Jun 11, 2015

The thing that strikes me about this is, what problem are you trying to solve?

The disadvantages of perpetuating this "overloading" into JavaScript is that it increases the emitted code footprint dramatically and emits items that are very hard to optimise out in a subsequent build step in another tool chain. Why are you trying to place this unnecessary overhead on every other user of TypeScript so you can just perpetuate 100% of the language features of TypeScript into JavaScript?

@zpdDG4gta8XKpMCd
Copy link
Author

increases the emitted code footprint dramatically

can't see how naming without overloading that i originally suggested (with handpicked effective name) is better as far as the size of footprint?

emits items that are very hard to optimise out in a subsequent build step in another tool chain

can you elaborate on this?

@jhchen
Copy link

jhchen commented Jun 11, 2015

inconvenience is mild and a one time thing

This is probably the source of disagreement. In my experience I’ve found it to be true that naming is one of the few hard problems in computer science.

the syntax might or might not be intuitive, subject for discussion

Agree this is subjective. In the current form it resembles namespace syntax in other languages but agree the specifics could be discussed. However, I’m not aware of any other language has a syntax specification purely for the benefit of generated code, and since intuition is informed by experience, in my opinion the idea of a nominal+effective name itself is also unintuitive.

primary goals of typescript are met (readability of the generated code)

This should be weighed against other primary goals like developer productivity and source code maintainability.

effective name can be optional

This may be the right compromise but it’s worth noting this path is still not free. There’s still the implementation effort for the Typescript team, the subsequent documentation and support, and the cognitive overhead for the developers that read the manual in full.

But if this is the desirable route then I think this can be two separate Github Issues? One for function overloading supported by compiler generated names, and another Issue for the optional nominal+effective syntax to control the generated names.

@zpdDG4gta8XKpMCd
Copy link
Author

naming is one of the few hard problems in computer science

agree, good and consistent naming is a hard thing to come by (that's why we are here talking about this feature), however not-as-good yet meaningful names aren't too hard to make up, and this is what handpicked effective names are, just good enough for the job to get done

since intuition is informed by experience

i am glad we pay so much attention to very personal subjective things, let's ask your experience one more time, how many languages do you know that proclaim readable generated code as one of their goals?

this path is still not free

i am with you on that, didn't you just say that naming is one of the hardest problems? all right.. so do you truly believe that using names provided by the developer is more work than developing a heuristic algorithms for making good looking names automatically?

by saying so, i conclude, you must have already developed one for all crazy signatures you can see in the wild:

function format(value: { value: typeof value }): string { return undefined; }

function format(value: Either<Promised<Optional<Workload>>, [{ value: string }, { value: number }, Promised<boolean[]>]>): string { return undefined; }

if so you should not hesitate and include it as a part of the formal proposal

@RyanCavanaugh RyanCavanaugh added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. and removed In Discussion Not yet reached consensus labels Jun 12, 2015
@RyanCavanaugh
Copy link
Member

Can you fill in the emit for this block?

var x = someCondition ? format : someOtherFunction;
x("");
x(32);

function fn(x: any) { /* some body */ }
fn(format);

var y: any = someCondition ? "" : 32;
format(y);

@zpdDG4gta8XKpMCd
Copy link
Author

No problem:

// if there is a common (comparible?) signature between someOtherFunction and one of the overloads
// then x is of the type of such signature, otherwise we get an unresolved type error
var x = someCondition ? format : someOtherFunction; 
x("");
x(32);

function fn(x: any) { /* some body */ }
fn(format); // <-- type error, resolving from nominal to effective is based on the signature which is erased here

var y: any = someCondition ? "" : 32;
format(y); // <-- if there is an overload for `string|number` no problem, otherwise a type error
/// UPDATE: didn't see `any`, if there is an overload for `any` then we use it, otherwise a type error 

@kitsonk
Copy link
Contributor

kitsonk commented Jun 12, 2015

No problem:

That isn't an emit... that is copying TypeScript and adding some comments.

can't see how naming without overloading that i originally suggested (with handpicked effective name) is better as far as the size of footprint?

Every "overload" will emit a full function. Current implementation means that in a lot of use cases dealing with permutations of arguments can be handled in one line of code. Why put the overhead of a full function on the the developer in TypeScript and in the emitted code? Your proposal does not preserve current functionality, so I would have no choice of having to use your suboptimal solution to utilise overloading in TypeScript. No thank you...

emits items that are very hard to optimise out in a subsequent build step in another tool chain

can you elaborate on this?

Most code optimisation in JavaScript works off tree pruning, where code branches that are unaccessible are trimmed (on top of other minimisation functions). It is typical that only advanced compiler functions would optimise out unused functions, and even then, if I have a library that I am planning on using, the final code would likely not have optimised out any of these functions. So again, if I can resolve the permutations of the overloaded argument with a single line of code, I still have to bear the burden of all these emitted functions, which increase the memory foot print of the code. I could hope and pray that the JIT is smarter.

You still didn't answer my question about what problem you are trying to solve... I also find it hard to see how this could even be considered an improvement over the current implementation.

@zpdDG4gta8XKpMCd
Copy link
Author

That isn't an emit... that is copying TypeScript and adding some comments.

if you look closely there is nothing to emit either because of an obviously unresolvable situation or lack of details, i believe all these corner cases were picked to illustrate possible difficulties, a typical situation (when a signature is clear and known) is as easy as pie

Your proposal does not preserve current functionality.

it looks like you are not getting the idea, there is no overhead and nothing that breaks how typescript currently works as long as you avoid using this feature, no breaking changes at all

Current implementation means that in a lot of use cases dealing with permutations of arguments can be handled in one line of code... Why put the overhead of a full function on the the developer in TypeScript and in the emitted code?

indeed currently available overloading (resolved at runtime) do require examining aruments which is an optimization killer, since no assumptions can be made as for what agruments array is every single time the function is called: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments

what i am suggesting is 180 degrees opposite, let's not do runtime overloading if it can be done statically at the compile time and then optimized no problem at runtime, how hard is that?

this feature is a good example of a zero runtime cost abstraction which is achieved 100% at the compiler's expense

optimise out unused functions

unused function should be removed at the compile time since all information is available for doing static analysis, again this feature has nothing to do with a number of unused functions that would have been there anyway just named differently (uniquely)

You still didn't answer my question about what problem you are trying to solve...

um... didn't you happen to hear about overloading at all?

here are a few links to start with:

a problem being solved:

  • allowing a user to address different functions by the same name
    • hence saving some effort of making up a unique name that fits nicely into the naming convention in place
    • improving readability of the code by eliminating a need for long unique names

@RyanCavanaugh
Copy link
Member

Keep it unsarcastic, please.

@kitsonk
Copy link
Contributor

kitsonk commented Jun 12, 2015

unused function should be removed at the compile time since all information is available for doing static analysis, again this feature has nothing to do with a number of unused functions that would have been there anyway just named differently (uniquely)

Not if you are exporting the functions.

um... didn't you happen to hear about overloading at all?

I understand the concept of overloading, which TypeScript supports. What I am unclear on is what problem are you trying to solve that the current implementation doesn't cover. You seem to indicate in your further comments that it is "allowing a user to address different functions by the same name" in the emitted JavaScript. If you are trying to solve that, I am not sure why.

You also indicate that "indeed currently available overloading (resolved at runtime) do require examining aruments which is an optimization killer" though I am having a difficult time conceiving where your suggested approach would on average be more optimised. Are there some examples of the current implementation and your suggested implementation where you can highlight how the suggestion would improve optimisation?

(If any of my comments are being perceived as sarcastic, I apologise, not my intent)

@zpdDG4gta8XKpMCd
Copy link
Author

Not if you are exporting the functions.

Even if you are exporting them, given that you have the entire code base at hands, you can see what's used and what is not. For libraries it's a different story.

What I am unclear on is what problem are you trying to solve that the current implementation doesn't cover.

Current TypeScript implementation allows function overloading that has to be resolved at runtime by examining arguments.

My idea is to leverage the type system and do overload resolution at the compile time basing it on function signatures. This way we have 0 performance penalty at runtime and convenience of using the overloaded functions in TypeScript that will be resolved, renamed, and emmitted as uniquely named functions in JavaScript - all by the compiler without having to do anything at runtime. Effectively it means that by looking at the generated JavaScript code you won't be able to tell whether a given function was overloaded or it was not in the source code in TypeScript. No trace whatsoever. This is what I mean by a zero cost abstraction 100% done by the compiler.

@AlicanC
Copy link

AlicanC commented Jul 14, 2015

I think the implementation of "TypeScript should know what I want to call" is a bit problematic. Instead, TypeScript could check what can be type checked during runtime and automatically emit runtime-type-checked functions from overloads.

Curent Situation

interface User {
  name: string;
}

function greet(message: string, userName: string); // Not emitted
function greet(message: string, user: User); // Not emitted
function greet(message: string, userOrUserName: any) { // Emitted without type info

  let userName: string;

  if (typeof userOrUserName === 'string') { // Manual checking
    userName = <string>userOrUserName;
  } else { // Can't check User, but it can be assumed
    userName = (<User>userOrUserName).name;
  }

  console.log(`${message}, ${userName}`);

}

Proposal

interface User {
  name: string;
}

function greet(message: string, userName: string) {
  console.log(`${message}, ${userName}`);
}

function greet(message: string, user: User) {
  greet(message, user.name);
}

Emitting Process

  • In every scope, collect call signatures for functions with the same name. (callSignatures[funcName].push(callSignature))
  • Before collecting, check if the list of signatures are runtime checkable.
    • Allow any number of signatures which can be runtime checked with typeof or instanceof or whatever.
    • Allow only one signature which can't be runtime checked. (We can "else" it.)

Emitted Code

function greet(message, userOrUserName) {
  // Emit overloads
  var overload_1 = function (message, userName) {
    console.log(message + ', ' + userName);
  };

  var overload_2 = function (message, user) {
    greet(message, user.name);
  };

  // Overloads we have are (message: string, userOrUserName: string) and (message: string, userOrUserName: User)

  // No need to check "message" since it's "string" in all overloads

  // Check "userOrUserName"
  if (typeof userOrUserName === 'string') { // String can be checked in runtime
    return overload_1(message, userOrUserName);
  } else { // User can not be checked in runtime, but it's no problem since we can "else" it
    return overload_2(message, userOrUserName);
  }

}

More Examples

interface Friend {
  name: string;
}

interface Pet {
  name: string;
}

/* Error! No way to check Friend and Pet in runtime. */
function brag(friend: Friend) {
  alert(`I have a cool friend named ${friend.name}.`);
}

function brag(pet: Pet) {
  alert(`I have a cool pet named ${pet.name}.`);
}

/* No problem. Can easily be done with a "x === undefined" check.  */
function bragFriend() {
  alert(`I have cool friends.`);
}

function bragFriend(friend: Friend) {
  alert(`I have a cool friend named ${friend.name}.`);
}

/* No problem. Can easily be done with a "typeof x === 'string'" check.  */
function bragFriend_2(name: string) {
  alert(`I have a cool friend named ${name}.`);
}

function bragFriend_2(friend: Friend) {
  alert(`I have a cool friend named ${friend.name}.`);
}

@AlicanC
Copy link

AlicanC commented Jul 14, 2015

I am not suggesting that the current style of overloading should be dropped. This should work:

interface Braggable {
  name: string;
}

interface Friend extends Braggable {
  lastName: string;
}

interface Pet extends Braggable {
  breed: string;
}

interface Boat extends Braggable {
  model: string;
}

function brag(name: string) { // Has to be "auto-runtime-checkable"
  alert(`I have a cool thing named ${name}.`);
}

function brag(friend: Friend); // Just needs to be compatible with (braggable: Braggable)
function brag(pet: Pet); // Just needs to be compatible with (braggable: Braggable)
function brag(braggable: Braggable) { // Has to be "auto-runtime-checkable"

  // Manual checks have to be done
  if (braggable.lastName) {
    alert(`I have a cool friend named ${braggable.name}.`); 
  } else if (braggable.breed) {
    alert(`I have a cool pet named ${braggable.name}.`); 
  }
}

@ToastHawaii
Copy link

I love the suggestion from AlicanC. This feature is very useful for the peoples from the C# and Java world and it makes the TypeScript code more readable.

I have tried to think this through and I would like to share my results.

Backward compatible

As AlicanC wrote the feature is full backward compatible, so the following example would still work.

function f();
function f(n: number);
function f(n?: number) {
    if(n) {
        alert("Argument n is " + n);
    } else {
        alert("None arguments.");
    }
}

Simple example

But it is difficult to read. A better way is to write it like this.

function f() {
    alert("None arguments.");
}

function f(n: number) {
    alert("Argument n is " + n);
}

this could compile to

// The compiled function contains none arguments. Instate the arguments object is used.
function f() {

    const _overload1 = function() {
        alert("None arguments.");
    };
    const _overload2 = function(n) {
        alert("Argument n is " + n);
    }

    // If a argument is undefined it means it is not set. The same as with optional parameters.
    if(arguments[0] === void 0) {
        // Every overload is wrapped with "return (...).apply(this, arguments);" this works for functions and class methods (and works as expected for "strict mode").
        return _overload1.apply(this, arguments);
    } else {
        // Every call ends in a overlade call. Even if there is no exact match.
        return _overload2.apply(this, arguments);
    }
}

Note:
Another way would be to use anonymous functions "return (function(...){...}).apply(this, arguments);" but for Union Types, Optional and Default Parameters and Rest Parameters this leads to redundanc.

Process

  1. In every scope, collect call signatures for functions with the same name.
  2. Order and group the list of signatures
    • by number of arguments

    • then by types starting with the first argument

      • boolean,
      • enum,
      • number,
      • string literal type,
      • string,
      • function (from less excepted number of arguments to most excepted number of arguments),
      • object (class (from super type to base type), tuple, typed array, array, interface),
      • any

      Note:
      Union Types, Optional and Default Parameters and Rest Parameters need some special handling. They have to be added more then once.

  3. Check if the groups of signatures are automatically runtime checkable.
    • Not allow mixing from automatically runtime check overloading with current overloading.
    • Allow any number of signatures that can be automatically runtime checked.
    • Allow one signature in any group that is not automatically runtime checkable.
    • Not allow two equally signature checks.
  4. Merge overloadings in one function.
    • Make only checks that are necessary to differ between overloads.
    • Make checks so that every call ends in the overlade with the best match. (Priority: 1. Number of arguments, 2. Type of arguments)

List of automatically runtime checks

The first line is the argument expression. The second line is a suggestion for the automatically runtime check.

Note:
"a" in the second line is a placeholder for "arguments[x]".

Basic

a: any
a !== void 0 // check if a argument exists

// or "a === void 0" for a argument does not exists.

Note:
If a argument is undefined it means it is not set. The same as with optional parameters.

Primitives

For primitives the typeof can be used.

If an argument is null it means it is set. As I know, every type is compatible with null.

a: boolean
a === null || typeof(a) === "boolean"

a: number
a === null || typeof(a) === "number"

a: string
a === null || typeof(a) === "string"

Note:
I do not know what the status of "Symbol" in TypeScript is.

Classes

For classes the instanceof can be used.

a: Date
a === null || a instanceof Date

Arrays

Arrays are classes so instanceof can be used.

a: any[]
a === null || a instanceof Array

Functions

For functions, the typeof can be used and additionally .length to check the number of expected arguments.

a: () => any
a === null || typeof(a) === "function" && a.length <= 0

a: (b:any) => any
a === null || typeof(a) === "function" && a.length <= 1

Extended

Note:
An part of this checks use checks from the basic.

Enums

a: Color // Color is an enum
a === null || typeof(a) === "number" && Color[a] !== void 0
// or if every number a valid enum value then
// a === null || typeof(a) === "number"
// but then there is none difference between the check for enum and number parameters...

String literal types

a: "s"
a === null || a === "s"

Typed arrays

For typed arrays the .every() method can be used to check the type from all elements.

a: number[]
a === null || a instanceof Array && a.every(e => e === null || typeof(e) === "number")

Note:
For older ECMAScript a polyfill for .every() is needed.

Tubles

a: [number, string]
a === null || a instanceof Array && a.length === 2 && (a[0] === null || typeof(a[0]) === "string") && (a[1] === null || typeof(a[1]) === "number")

Interfaces

// Interfaces are tricky. Is there really a good way to check automatically...?
// For simple interfaces that might work.
a: {x: number, y: number}
a === null || (a.x === null || typeof(a.x) === "number") && (a.y === null || typeof(a.y) === "number")

Union types

a: number | Date
(a === null || typeof(a) === "number") || (a === null || a instanceof Date)

Note:
Signatures with a union type parameter has to be added twice or more.
Once for (a === null || typeof(a) === "number") and once for (a === null || a instanceof Date).

Optional and Default Parameters

a?: number
(a === void 0) || (a === null || typeof(a) === "number")

a: number = 1
(a === void 0) || (a === null || typeof(a) === "number")

Note:
Signatures with a optional and default parameters parameter has to be added twice.
Once for (a === void 0) and once for (a === null || typeof(a) === "number").

Rest Parameters

...a: any[]
// Every state of arguments is valid for rest parameters.

...a: number[]
Array.prototype.slice.call(arguments, 0 /* Start index from the rest parameter */).every(e => e === null || typeof(e) === "number")

Note:
Signatures with a rest parameters has to be added to every number of arguments group if in this group not an other with the same type.

Generics

a: T // T is a generic type
// For generic type parameters at compile time the type is known, so the runtime check for this can be used.

a: C<T> // C is a class and T is a generic type.
a === null || a instanceof C
// For a generic class type parameters only the class can be checked.

Complex example

function f() {
    alert("none arguments");
}

function f(n: number) {
    alert("n is " + n);
}

function f(s: string) {
    alert("s is " + s);
}

function f(d: Date) {
    alert("The iso from d is " + d.toISOString());
}

function f(a: any[]) {
    alert("The length of a is " + a.length);
}

function f(b: boolean, s: string) {
    alert("b is " + b + " and s is " + s);
}

function f(n: number, d: Date) {
    alert("n is " + n + " and the iso from d is " + d.toISOString());
}

function f(n: number, i: {x:number, y:number}) {
    alert("n is " + n + " and i.x is " + i.x + " and i.y is " + i.y);
}

this could compile to

function f() {
    const _overload1 = function () {
        alert("none arguments");
    };
    const _overload2 = function (n) {
        alert("n is " + n);
    };
    const _overload3 = function (s) {
        alert("s is " + s);
    };
    const _overload4 = function (d) {
        alert("The iso from d is " + d.toISOString());
    };
    const _overload5 = function (a) {
        alert("The length of a is " + a.length);
    };
    const _overload6 = function (b, s) {
        alert("b is " + b + " and s is " + s);
    };
    const _overload7 = function (n, d) {
        alert("n is " + n + " and the iso from d is " + d.toISOString());
    };
    const _overload8 = function (n, i) {
        alert("n is " + n + " and i.x is " + i.x + " and i.y is " + i.y);
    };
    if (arguments[0] === void 0) {
        return _overload1.apply(this, arguments);
    } else if (arguments[1] === void 0) {
        if (arguments[0] === null || typeof (arguments[0]) === "number") {
            return _overload2.apply(this, arguments);
        } else if (typeof (arguments[0]) === "string") {
            return _overload3.apply(this, arguments);
        } else if (arguments[0] instanceof Date) {
            return _overload4.apply(this, arguments);
        } else {
            return _overload5.apply(this, arguments);
        }
    } else {
        if (arguments[0] === null || typeof (arguments[0]) === "boolean") {
            return _overload6.apply(this, arguments);
        } else {
            if (arguments[1] === null || arguments[1] instanceof Date) {
                return _overload7.apply(this, arguments);
            } else {
                return _overload8.apply(this, arguments);
            }
        }
    }
}

Critical examples

Function

function f(a: (a: number) => string) {
    alert("a returns " + a(1));
}

/* Error! No safe way to automatically check at runtime. */
function f(a: (a: string) => string) {
    alert("a returns " + a("a"));
}

Interface

interface I1 {
    p: string;
}

interface I2 {
    p: string;
}

function f(i: I1) {
    alert("i.p of i:I1 is " + i.p);
}

/* Error! No safe way to automatically check at runtime. */
function f(i: I2) {
    alert("i.p of i:I2 is " + i.p);
}

Union type

function f(b: boolean) {
    alert("b is " + b);
}

/* Error! No safe way to automatically check at runtime. */
function f(bs: boolean | string) {
    alert("bs is " + bs);
}

Optional and Default Parameters

function f(b: boolean) {
    alert("b is " + b);
}

/* Error! No safe way to automatically check at runtime. */
function f(b?: boolean = true) {
    alert("b? is " + b);
}

Enum and number

enum E {
    v1,
    v2
}

function f(e: E) {
    alert("e is " + e);
}

function f(n: number) {
    alert("n is " + n);
}

this could compile to

var E;
(function (E) {
    E[E["v1"] = 0] = "v1";
    E[E["v2"] = 1] = "v2";
})(E || (E = {}));

function f() {

    const _overload1 = function(e) {
        alert("e is " + e);
    };
    const _overload2 = function(n) {
        alert("n is " + n);
    };

    if(arguments[0] === null || typeof(arguments[0]) === "number" && E[arguments[0]] !== void 0) {
        return _overload1.apply(this, arguments);
    } else {
        return _overload2.apply(this, arguments);
    }
}

but is it save to compile this?

Array and tuple

function f(t: [number, number]) {
    alert("The t[0] of t is " + t[0] + " and t[1] is " + t[1]);
}

function f(a: number[]) {
    alert("The length of a is " + a.length);
}

this could compile to

function f() {

    const _overload1 = function(t) {
        alert("The t[0] of t is " + t[0] + " and t[1] is " + t[1]);
    };
    const _overload2 = function(a) {
        alert("The length of a is " + a.length);
    };

    if(a === null || a instanceof Array && a.length === 2) {
        return _overload1.apply(this, arguments);
    } else {
        return _overload2.apply(this, arguments);
    }
}

but is it save to compile this?

Rest parameters

function f(n: number) {
    alert("n is " + n);
}

function f(s: string, n: number) {
    alert("s is " + s + " n is " + n);
}

function f(...r: number[]) {
    alert("The length of r is " + r.length);
}

this could compile to

function f() {

    const _overload1 = function(n) {
        alert("n is " + n);
    };
    const _overload2 = function(s, n) {
        alert("s is " + s + " n is " + n);
    };
    const _overload3 = function(...r) {
        alert("The length of r is " + r.length);
    };

    if (arguments[0] === void 0) {
        return _overload3.apply(this, arguments);
    } else if (arguments[1] === void 0) {
        return _overload1.apply(this, arguments);
    } else if (arguments[2] === void 0) {
        if (arguments[0] === null || typeof (arguments[0]) === "number") {
            return _overload3.apply(this, arguments);
        } else {
            return _overload2.apply(this, arguments);
        }
    } else {
        return _overload3.apply(this, arguments);
    }
}

but is it save to compile this?

Summery

I think it is a very useful feature for developers who do not work every day with Typescript or JavaScript.

But I also understand that this approach is fuzzy and not perfect. This kind of overloading support is not the JavaScript way to do it.

@evictor
Copy link

evictor commented Feb 26, 2016

Here's a simple example of common overloading desires that TypeScript confounds:

Desired code (arguably totally unambiguous what the intent here is):

function alertSomething(str: string): void {
  alert(str);
}

function alertSomething(num: number): void {
  alertSomething(num.toString());
}

Yet you cannot do this in TypeScript. What you must do is cause type information and thus the usefulness of the compiler to be lost in the body of the single overload implementation:

function alertSomething(str: string): void;
function alertSomething(num: number): void;
function alertSomething(whoKnows: any): void {
  if(whoKnows instanceof number)
    alert(whoKnows.toString());
  else if(whoKnows instanceof string)
    alert(whoKnows);
  else
    throw 'As a programmer I should not have to worry about handling this runtime typing and exceptioning';
}

Honestly this is dead obvious to me and the one thing about TypeScript that really bothers me.

@kitsonk
Copy link
Contributor

kitsonk commented Feb 26, 2016

Honestly this is dead obvious to me and the one thing about TypeScript that really bothers me.

It is dead obvious to you, but in your first example, what would you propose as the dead obvious emit?

@AlicanC
Copy link

AlicanC commented Feb 26, 2016

// Overload 1
function alertSomething__1(str) {
  alert(str);
}

// Overload 2
function alertSomething__2(num) {
  alertSomething(num.toString());
}

// Overload Wrapper
function alertSomething(__whatever) {
  if (typeof __whatever === 'string') {
    return alertSomething__1.call(this, __whatever);
  } else if (typeof __whatever === 'number') {
    return alertSomething__2.call(this, __whatever);
  }
}

This really doesn't sound so magical to me. Are we missing something?

@kitsonk
Copy link
Contributor

kitsonk commented Feb 26, 2016

@AlicanC

TypeScript Goals:

  1. Impose no runtime overhead on emitted programs.

TypeScript Non-Goals:

  1. Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

But putting that aside:

function alertSomething({ foo: 'bar' }); // what happens?

And what would you emit here:

interface Foo {
    foo: string;
}

interface FooFunction {
    (): Foo;
}

function alertSomething(obj: Foo): void {
    alert(obj.foo);
}

function alertSomething(fn: FooFunction): void {
    alert(fn().foo);
}

function alertSomething(obj: Object): void {
    alert(JSON.stringify(obj));
}

function alertSomething(str: string): void {
    alert(str);
}

function alertSomething(num: number): void {
    alert(String(num));
}

alertSomething({ foo: 'foo', bar: 'bar' });

@shmuelie
Copy link

I think another point from the Design Goals is the key here:

  1. Provide an end-to-end build pipeline. Instead, make the system extensible so that external tools can use the compiler for more complex build workflows.

This should be something that is done by a "preprocessor", maybe even related to #6508

@evictor
Copy link

evictor commented Feb 26, 2016

@kitsonk The code path is entirely unambiguous; the param types are part of a function's signature and serve to disambiguate resolution. I.e. a call to alertSomething(x: number) results in a call to alertSomething(x: string) which in its body does alert(x: string).

In your example, the answer is it's a compile time error—unable to resolve which overload to use. This is perfectly reasonable and is the behavior in many languages which allow this practice. If you were so inclined, then you could disambiguate your call with the foobar object by typing it in an unambiguous way, such as assigning it to a var explicitly typed as a Foo, or an Object, or what have you.

@kitsonk
Copy link
Contributor

kitsonk commented Feb 26, 2016

@evictor if it is unambiguous, what would you propose as an emit?

@evictor
Copy link

evictor commented Feb 27, 2016

It would be difficult to approach renaming the overloading functions as someone else in this thread suggested because external things could be relying on the one name. I think the best way to do it would be to write out exactly what a human has to write now in order to do this runtime "pseudo-overloading" and keep the original method name so no external references to the symbol need to be changed, e.g.:

// Theoretical TypeScript as before
function alertSomething(str: string): void {
  alert(str);
}

function alertSomething(num: number): void {
  alertSomething(num.toString());
}
// Emitted JavaScript
function alertSomething() {
  if(typeof arguments[0] == 'string')
    alert(arguments[0]);
  else if(typeof arguments[0] == 'number')
    alertSomething(arguments[0].toString());
  else
    throw 'TypeScript runtime error; unexpected arguments to alertSomething()';
}

Then of course this could be extended so that number of arguments could be considered, etc. This is a viable solution because you can compile that down right there into the resultant JavaScript without having knowledge of external callers using whichever overloads (since the symbol name ultimately stays the same), and ultimately the end user (programmer) would end up writing exactly that sort of runtime type checking anyway.

@ToastHawaii
Copy link

This is dangeres if you have varibales mit the same name in the function.
I suggest:

function alertSomething() {

    const _overload1 = function(num) {
        alertSomething(num.toString());
    };
    const _overload2 = function(str) {
        alert(str);
    };

    if (typeof arguments[0] === "number") {
        return _overload1.apply(this, arguments);
    } else {
        // I think every call sould end in a overload
        return _overload2.apply(this, arguments);
    }
}

Or

function alertSomething() {

    const _overload1 = function(str) {
        alert(str);
    };
    const _overload2 = function(num) {
        alertSomething(num.toString());
    };

    if (typeof arguments[0] === "string") {
        return _overload1.apply(this, arguments);
    } else if (typeof arguments[0] === "number"){
        return _overload2.apply(this, arguments);
    } else {
        throw "TypeScript runtime error; unexpected arguments to alertSomething()";
    }
}

@Chris2011
Copy link

Chris2011 commented May 30, 2016

We had some discussion in the slack chat of https://angularjs-de.slack.com (Typescript chan) I used GWT for a long time and I wondered by myself, why is real overloading in TS not possible, but in GWT. Both generated JS code. So someone came up with an idea, how GWT could do this (Only an idea)

Java (Used in GWT)

public String foo(String a) { ... }
public String foo(int a) { ... }

JavaScript (Output)

function foo_string(a) { ... }
function foo_int(a) { ... }

Using:

Java (Used in GWT)

String bar0 = foo("bar");
String bar1 = foo(1);

JavaScript (Output)

var bar0 = foo_string("bar");
var bar1 = foo_int(1);

And this is similar to this comment: #3442 (comment)

So it would be very handy, if we can have this in TypeScript too. I heard about the philosophy to not generate stuff which is not supported in JS at all, but if you generate the code, concate the files, minify the files (e.g. for a spa) than you don't want to read the code which was generated anymore. You only write in TS.

@mhegazy
Copy link
Contributor

mhegazy commented Jun 4, 2016

This proposal violates multiple of the TypeScript design goals (https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#goals); specifically. breaking changes (11), changing behavior of a JS program (7), and, though a subjective argument, generates not-pretty-code (4).

The other issue is it relies on type-directed emit. something that would break single file transpilation scenarios.

for all of these, this proposal is out of scope for the TypeScript project for the time being.

@mhegazy mhegazy closed this as completed Jun 4, 2016
@mhegazy mhegazy added Out of Scope This idea sits outside of the TypeScript language design constraints and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Jun 4, 2016
@Shlomibo
Copy link

Shlomibo commented Jan 13, 2017

Why require the dispatch to be static? It would not fit into JS world.
We also don't have to generate the code for dynamic dispatch. The programmer already doing it.

Yet, IMHO we can still have better type-checking and validations over overloaded functions:
#12041

@G1itcher
Copy link

G1itcher commented Nov 25, 2017

Why can't function overloading simply be implemented as syntactic sugar for the regular single function with the standard pattern of typeof/instanceof checking?

i.e.

function foo(x: number){/*body Foo 1*/}
function foo(x: string, y: number){/*body Foo 2*/}

emits to

function foo() {
    var args = [];
    for (var _i = 0; _i < arguments.length; _i++) {
        args[_i] = arguments[_i];
    }
    if (args.length === 1 &&
        typeof args[0] === "number") {
        /*body Foo 1*/
    }
    else if (args.length === 2 &&
        typeof args[0] === "string" &&
        typeof args[1] === "number") {
        /*body Foo 2*/
    }
}

obviously, the emit isn't beautiful, but it's certainly readable and is currently what is emitted for function(...args) anyway. An alternative would be to boil down an overloaded function into the equivalent Typescript function defined with optionals and unions

i.e.

function foo(x: number){/*body Foo 1*/}
function foo(x: string, y: number){/*body Foo 2*/}

would be equivalent to

function foo(x: number | string, y?: number) {
    if (typeof x === "number" && y === null) {
        /* Foo body 1*/
    }
    else if (typeof x === "string" && y != null) {
        /* Foo body 2*/
    }
}

which emits to


function foo(x, y) {
    if (typeof x === "number" && y === null) {
        /* Foo body 1*/
    }
    else if (typeof x === "string" && y != null) {
        /* Foo body 2*/
    }
}

Am I oversimplifying this issue? Obviously making Typescript enforce typings and usage of the overloaded functions in IDE and compile time is a different problem, but with my limited knowledge, it seems like a half solved problem?

@kitsonk
Copy link
Contributor

kitsonk commented Nov 26, 2017

@G1itcher that would work for a very limited use cases. What about something like this?

function foo(x: string[]): void;
function foo(x: { [key: string]: string }): void;

Ultimately, your scenario would only work when primitive values are used as arguments. Also, you suggestion doesn't cater for overloading of return types.

@fletchsod-developer
Copy link

You know, the use of get property and set property show that this method overloading enhancement can be done. Not terribly difficult an enhancement for TypeScript language.

@kitsonk
Copy link
Contributor

kitsonk commented Nov 27, 2017

You know, the use of get property and set property show that this method overloading enhancement can be done.

You know, that is valid JavaScript syntax, versus some special TypeScript construct. That isn't function overloading, that is interpreting JavaScript syntax.

As Mohamed stated above it isn't the technical difficulty, it is that it break multiple design goals and non-goals of TypeScript.

@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests