Skip to content

Suggestion: A better and more 'classic' function overloads for TypeScript #12041

Description

TypeScript Version: 2.0.3 / nightly (2.1.0-dev.201xxxxx)

Today's function overloading in TypeScript is a compromise which is the worst of both worlds of dynamic, non-overloaded language like JS, and static, overloaded language such as TS.

Static languages weakness:
In a static language such as TypeScript or C#, most types must be provided with the code. This put some burden on the programmer.

class Example
{
    //            ******          ***
    public static string Overload(int num)
    {
        return num.ToString();
    }
    //            ******          ******      ***
    public static string Overload(string str, int num)
    {
        return str + num.ToString();
    }
}

JS weakness:
In JS, there are no function overloads as they cannot be inferred from each other. All parameters are optional and untyped.
Because of that the programmer has to provider the logic for inferring which functionality is desired, and execute it. If the code should be divided into more specific functions, they must be named differently.

function overload(numOrStr, optionalNum) {
	return typeof numOrStr === 'number'
		? implementation(numOrStr)
		: otherImplementation(numOrStr, num);
}
function implementation(num) {
	return num.toString();
}
function otherImplementation(str, num) {
	return str + num;
}

Nothing in the code would suggest to a future programmer that the last two functions are related.

TypeScript has both problems, and more...
Because TS is bound to JS, only one overloaded function can have a body, and that function must has parameters that are compatible with all other overloads.
That function must resolve the desired behavior by the the parameters` types and count.

                       ******   ******
function overload(num: number): string;
                       ******       ******   ******
function overload(str: string, num: number): string;
                            ***************        ******   ******
function overload(numOrStr: number | string, num?: number): string {
    return typeof numOrStr === 'number'
    ? implementation(numOrStr)
                                    *   **********                   *
    : otherImplementation(numOrStr, (num as number /* are we sure? */));
}
                             ******
function implementation(num: number) {
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return num.toString();
}
                                  ******       ******
function otherImplementation(str: string, num: number) {
    // !! Must validate the types of input parms!
    return str + num;
}

Look how much longer it is than both previous examples, but with providing little benefits to code readability and maintenance, if any.
We could shorten it a bit by disabling type checking in the real overload, but then we loose type checking for the compatibility of the overloads.

Either way, there's no checking that the programmer actually handles all the declared overloads.

                       ******   ******
function overload(num: number): string;
                       ******       ******   ******
function overload(str: string, num: number): string;
function overload(numOrStr, num) {
    return typeof numOrStr === 'number'
    ? implementation(numOrStr)
    : otherImplementation(numOrStr, num);
}
                             ******
function implementation(num: number) {
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return num.toString();
}
                                  ******       ******
function otherImplementation(str: string, num: number) {
    // !! Must validate the types of input parms!
    return str + num;
}

My suggestion: at least lets have validation that overloads are handled, and enjoy the better semantics of the static languages.

Syntax:
Like current TS overloads, all overloads must be specified one after another. No other code can separate them.
Unlike current TS overloads, all overloads must have a body.

// OK, current syntax
function overload(num: number): string;
function overload(str: string, num: number): string;
function overload(numOrStr, num) {
    // code
}
-------------------------------------------------------------
// OK, new syntax
function overload(num: number): string {
    // code
}
function overload(str: string, num: number): string {
    // code
}
function overload(numOrStr, num) {
    // code
}
------------------------------------------------------------
// ERROR. No mix and match
function overload(num: number): string; // Error: missing body
function overload(str: string, num: number): string {
    // code
}
function overload(numOrStr, num) {
    // code
}

function overload(num: number): string {
    // code
}
function overload(str: string, num: number): string; // Error: missing body
function overload(numOrStr, num) {
    // code
}
---------------------------------------------------------------
// ERROR: No code between overloads
function overload(num: number): string {
    // code
}
const iMNotSupposeToBeHere = 'xxx';
function overload(str: string, num: number): string { // Error: duplicate function implementation
    // code
}
function overload(numOrStr, num) {
    // code
}

For readability purpose, I would suggest the first overload would be the entry-overload. That is the function that is exposed to JS code, but is hidden from TS code.
That function would infer the desired behavior, and call the appropriate overload

function overload(numOrStr: number | string, num?: number): string {
    return typeof numOrStr === 'number'
        ? overload(numOrStr)
        : overload(numOrStr, (num as number));
}
function overload(num: number): string {
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return num.toString();
}
function overload(str: string, num: number): string{
    // !! Must validate the types of input parms!
    return str + num;
}

An overload must be called from reachable code in the entry-overload:

function overload(numOrStr, num) {
    return overload(numOrStr, num);
}
function overload(num: number): string { // ERROR: overload is not referenced from entry function.
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return num.toString();
}
function overload(str: string, num: number): string{
    // !! Must validate the types of input parms!
    return str + num;
}

In this syntax, the programmer has to provide an implementation that handles a declared overload, and each overload handle its parameters, validates them, and define the desired behavior.
The code implies that all these functions are related, and enforce that they would not be separated.

All other semantics and syntax regarding overload resolution, type checking, generics, etc' should remain the same as it is.

Emitted code:
The overloaded implementations should be moved into the scope of the entry-overload. A bit of name mangling is required because JS cannot support overloads - but any name-mangling may be used
From the previous example:

function overload(numOrStr: number | string, num?: number): string {
    return typeof numOrStr === 'number'
        ? overload(numOrStr)
        : overload(numOrStr, (num as number));
}
function overload(num: number): string {
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return num.toString();
}
function overload(str: string, num: number): string{
    // !! Must validate the types of input parms!
    return str + num;
}

We can very simply generate this code:

function overload(numOrStr, num) {
    return typeof numOrStr === 'number'
        ? overload$number(numOrStr)
        : overload$string$number(numOrStr, num);
// }  <- Oh, no
    // Well, it is unfortunate
    function overload$number(num) {
        // We must validate our input type.
        if (typeof num !== 'number') {
            throw new Error();
        }
        return num.toString();
    }
    function overload$string$number(str, num){
        // !! Must validate the types of input parms!
        return str + num;
    }
} // Ow, here I am

Notice that the output stays coherent.
Because the parameters are already in the scope of the implementations overloads, and if the respective parameter names are the same, or if renaming them would not introduce a naming conflict; we can omit the parameters and arguments from the implementation overloads.

function overload(numOrStr, num) {
    return typeof numOrStr === 'number'
        ? overload$number()
        : overload$string$number();

    // Well, it is unfortunate
    function overload$number() {
        // We must validate our input type.
        if (typeof num !== 'number') {
            throw new Error();
        }
        return num.toString();
    }
    function overload$string$number(){
        // !! Must validate the types of input parms!
        return str + num;
    }
}

We must also check if the entry-overload hides an otherwise captured variable by closure of the implementations. in this case, the hiding variable should be renamed.

const str = 'abc';

function overload(str, num) {

    return typeof str === 'number'
        ? overload(num)
        : overload(str, num);
}
function overload(num: number): string {
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return str + num;
}
function overload(str: string, num: number): string{
    // !! Must validate the types of input parms!
}

Should transpiled to something like

const str = 'abc';

function overload(_str, num) {

    return typeof str === 'number'
        ? overload$number(num)
        : overload$string$number(_str, num);

    function overload$number(num) {
        // We must validate our input type.
        if (typeof num !== 'number') {
            throw new Error();
        }
        return str + num;
    }
    function overload$string$number(str, num) {
        // !! Must validate the types of input parms!
    }
}

But I don't think this would be a common case.

I really really hope you would consider my suggestion.

Activity

  1. HerringtonDarkholme commented on Nov 4, 2016

    @HerringtonDarkholme
    Contributor

    Possible duplicate of #3442.
    IMHO, compile time generated function is possible contrary to TS's design philosophy

  2. Shlomibo commented on Nov 4, 2016

    @Shlomibo
    Author

    Herrington Darkholme (@HerringtonDarkholme)
    I'm not sure of that, but it might be true.
    Yet, IMHO, this syntax has much better semantics than the current syntax, and it's in par with TS design goals and philosophy (I think).

    Also, the two are not mutual exclusive.
    You can think of it as basic for that compile time generated function. If they would implement this, in the future, some implementation of compile-time generated entry-overload could fit into it as simple as writing that function yourself.

  3. aluanhaddad commented on Nov 6, 2016

    @aluanhaddad
    Contributor

    Like current TS overloads, all overloads must be specified one after another. No other code can separate them.

    That is not strictly true. Overloads should be specified in order from greatest to least specificity of their argument types but they can come from multiple sources making order non-deterministic.

    Looking at the example emitted code,

    function overload(numOrStr, num) {
        return typeof numOrStr === 'number'
            ? overload$number(numOrStr)
            : overload$string$number(numOrStr, num);
    ...

    How does the compiler know to emit that code?

    I'm not sure I see how this proposal improves the situation since it makes implementing the callee more complicated, but does not improve the experience for the caller (I think the current experience is just fine).

    Also, this is a massive breaking change, how should that be handled?

  4. Shlomibo commented on Nov 6, 2016

    @Shlomibo
    Author

    Aluan Haddad (@aluanhaddad)

    That is no strictly true. Overloads should be specified in order from greatest to least specificity of their argument types but they can come from multiple sources making order non-deterministic

    I haven't talked about ordering.
    What can be supplied from multiple sources are declarations, which are irrelevant to this case, and should not be changed (they are specifying what is already exist in code. Not what that is written for the current program.

    How does the compiler know to emit that code?

    It is in the source code that the transpiler compiles. You should compare the transpiled code to the source.
    Just like current implementation the programmer must resolve the desired overload (It's a JS limitation).

    What he gets here but not in the current syntax is better separations of concerns. Each overload handles its own data and behavior, like in any other language that provides function overloading.

  5. froh commented on Nov 25, 2016

    @froh

    name mangling proposal: md5-hash the normalized function signature

    I understand currently overloaded functions compile into just one function, which then at run time has to determine the actual parameter types and dispatch.

    I also understand a key issue for proper 1:1 compilation of overloaded functions to individual functions instead is name mangling (like it's done in C++ vs C, quite similar problem)

    Has anybody ever considered a two step process?

    1. flatten the signature, normalize and serialize it to a string.

      flatten means expand types down to basic types. normalize means e.g. sort fields alphabetically where order doesn't make a difference. serialize could e.g. be as json.

    2. md5-hash this flattened normalized signature

    use that md5 hash as a suffix to the function name. Accompany the compiled output with both the original, un-flattened signature, as well as the flattened one, as generated doc string comment, so other tools can remain useful with someFunction_7b1a4f7194c8063b353e45c96c4107ab vs someFunction_d32239bcb673463ab874e80d47fae504

    That should keep a good balance between keeping the generated code human readable (function names won't expand too much) yet as close as possible to 1:1 ES6. It's a bit like compiling C++ to C. If you want signature based function selection at compile time there is no way around name mangling. imho.

  6. Shlomibo commented on Nov 26, 2016

    @Shlomibo
    Author

    Susanne Oberhauser-Hirschoff (@froh)
    I feel that I have no saying on how to do the name mangling, except that I agree it is required by JS (as it is required in C++).

    Because from JS POV there's only one possible function, I don't think exposing the name-mangled overloads would be a good idea.
    It is interesting though what calls between overloads should be look like? Should TS emit code to call directly to that overload, or let the flow go through the entry-overload as all other calls?

  7. froh commented on Nov 26, 2016

    @froh

    Shlomi Borovitz (@Shlomibo)

    Generated calls should directly call the resolved overload.

    I agree the mangling could be any sufficiently unique compression, not just a hash, and md5 was just an example. But the key is: it has to be compact. very compact because (and that's where I kindly disagree) I strongly believe the mangled names then should go into the generated code: each overloaded function is emitted with it's mangled call signature. Each resolved call is emitted as a call to the right function. The compact mangling prevents emitted code from horribly exploding in length (as they would with C++ style mangling :-) ) a standardized mangling will make the emitted code accessible to tools.

    I find this less confusing and error prone than having to manually encode run time type checking and dispatch. this manual dispatch thing suggests the classical "if type1 do this, if type2 do that" that I hated to maintain in (cough) cobol back in the days.

  8. Shlomibo commented on Nov 27, 2016

    @Shlomibo
    Author

    For generated calls to be statically resolved, you have (IMHO) to have dynamic type-checker that ensures that the tun-time types are actually those that were statically resolved.

    The problem here is that you don't actually have dynamic type-checker, and at run-time an argument can have a value of entirely different type.
    You can't even impose rules to mitigate that because you're probably dependent on some 3rd-party(ies) library that wouldn't conform to such rules.

    But, I can't even see the benefit of such scheme.
    IMHO exposing implementation details (such as overload implementation) or otherwise extending your public interface supposed to be an intentional act, and therefor explicit.

    Now, a library writer whom wants to expose a specific overload may already do so with the current TS syntax, by giving that overload-implementation a name that mostly fits its semantics (rather then relying on some hard-to-predict name-mangling), and exporting that function.

    Using my suggestion may only benefit him by ensuring that only the overloads that are actually handled are exposed through the overloaded function.

    What are the benefits that are not already easy to achieve with the current syntax, that would be achieved by exposing overload-implementations?

  9. avchugaev commented on Jan 9, 2017

    @avchugaev
    Contributor

    Ideas of class methods overloading implementation:

    • TS compiler does not emit additional code that checks arguments length, params types, whatever.
    • TS compiler should emit JS function per each overloaded method, with unique name.
    • TS compiler decides which exactly method should be used based on arguments types (TS).
    • TS compiler emits error when it cannot clearly define which one of generated methods to use (if types definition is too weak / 'any' specified as type / etc). Thus, developers should refactor their code and make types definitions more exact.

    TypeScript code:

    // Interface declaration
    
    interface IGreeter {
        greet(name: string);
        greet(person: Person);
    }
    
    // Class Implementation
    
    class DummyGreeter {
        public greet(name: string) {
            console.log('Hi, ' + name);
        }
    
        public greet(person: Person) {
            this.greet(person.name);
        }
    }
    
    // Usage
    
    let dummyGreeter: DummyGreeter = new DummyGreeter();
    
    dummyGreeter.greet(new Person('Alex'));

    Transforms to:

    function DummyGreeter {
        
    }
    
    DummyGreeter.prototype.greet__1 = function(name) {
        console.log('Hi, ' + name);
    };
    
    DummyGreeter.prototype.greet__2 = function(person) {
        this.greet__1(person.name);
    };
    
    
    var dummyGreeter = new DummyGreeter();
    
    dummyGreeter.greet__2(new Person('Alex'));

    And everybody's happy. One step to traditional OOP and C# ;).

  10. avchugaev commented on Jan 9, 2017

    @avchugaev
    Contributor

    I think overloading is required attribute of each traditional OO programming language. Thus many developers will ask for it's implementation in TS again and again and again. As well as final keyword for classes and other features that mature OO programming language have.

  11. aluanhaddad commented on Jan 9, 2017

    @aluanhaddad
    Contributor

    @achugaev93
    It is interesting that you bring up C# here because it does in fact handle overloading correctly; handling the distinction between generic overloads as well as the distinction between overloading, overriding, and shadowing; while many other languages, say Java, get this wrong.

    I love C# but it is not a model for TypeScript. Even if it were, that wouldn't make TypeScript a "traditional OO language" because C# is fairly non-traditional.
    Regardless what makes you think this is a "traditional OO language" as you call it?

    flatten means expand types down to basic types. normalize means e.g. sort fields alphabetically where order doesn't make a difference. serialize could e.g. be as json.

    Susanne Oberhauser-Hirschoff (@froh) I'm not sure that a type can reasonably be expected to be reduced to primitives. Function types, union types, and generic types seem like they might be prohibitively difficult.

  12. avchugaev commented on Jan 9, 2017

    @avchugaev
    Contributor

    Aluan Haddad (@aluanhaddad) Lets skip discussions about other programming languages and focus on the topic of this thread. Modern and mature model of OOP includes such feature as method overloading. That's a fact. And many TS programmers stocked a box of beer and ready to shout "Hurrah!!!" every time TypeScript got another feature inherent in classical OO programming languages.

  13. aluanhaddad commented on Jan 9, 2017

    @aluanhaddad
    Contributor

    Lets skip discussions about other programming languages and focus on the topic of this thread.

    You introduced the comparison in no uncertain terms

    And everybody's happy. One step to traditional OOP and C# ;).

    Modern and mature model of OOP includes such feature as method overloading.

    Please explain how this is relevant.

    And many TS programmers stocked a box of beer and ready to shout "Hurrah!!!" every time TypeScript got another feature inherent in classical OO programming languages.

    Its interesting that the etymology of "classical" as used in this context actually arises from the need to distinguish JavaScript and by extension TypeScript from other languages that have fundamentally different semantics. Regardless the programmers you refer to need to learn JavaScript. If they already know JavaScript but think that TypeScript has a different object model then you should inform them that this is not the case.

  14. avchugaev commented on Jan 9, 2017

    @avchugaev
    Contributor

    Does this mean that the development of language will stop halfway to the classical OOP? Why such everyday functions as overloading methods, final classes, etc. cannot be implemented in the compiler?

  15. aluanhaddad commented on Jan 10, 2017

    @aluanhaddad
    Contributor
  16. 36 remaining items

  17. avchugaev commented on Jan 16, 2017

    @avchugaev
    Contributor

    So make an exception for methods overloading in the design decisions.

  18. avchugaev commented on Jan 16, 2017

    @avchugaev
    Contributor

    Sometimes it's necessary to break rules ;)

  19. aluanhaddad commented on Jan 16, 2017

    @aluanhaddad
    Contributor

    To add to this a bit, TypeScript and C# both have exceptional synergy with their respective runtimes. C#'s fantastic support for precise overloading is achieved through synergy with the CLR.
    For example, in the CLR generic type instantiations are reified, which is great, and C# leverages that to allow the following to works perfectly

    public static double Sum(IEnumerable<double> values);
    public static double? Sum(IEnumerable<double?> values);
    public static int Sum(IEnumerable<int> values);
    public static int? Sum(IEnumerable<int?> values);

    In Java, all of those overloads are compiler errors because it cannot differentiate between them due to generic erasure in the JVM.
    Trying to emulate the C# behavior in languages that target the JVM is problematic and complex.

  20. Shlomibo commented on Jan 16, 2017

    @Shlomibo
    Author

    Aluan Haddad (@aluanhaddad)
    We're not emulating anything, as the dynamic dispatch is still done by the user, as the underlying runtime (JS) expecting.

    We're only using existing rules for the small context where static resolution is acceptable: that is inside the entry-overload (which has no equivalent in C# - because we're not emulating it)

  21. aluanhaddad commented on Jan 16, 2017

    @aluanhaddad
    Contributor

    Just thought it was relevant food for thought on whether or not this is a good idea. This decision has already been firmly established by the TypeScript team.
    However, I find the discussion interesting in itself.

  22. Shlomibo commented on Jan 16, 2017

    @Shlomibo
    Author

    The decision is made different proposal, with demerits which I think this proposal working around them

  23. rhyek commented on Apr 27, 2017

    @rhyek

    Traditional method overloading is practically ubiquitous in OOP. Rather than being anal about some design philosophies made who knows how long ago (and which new users don't care about), try to make an exception every now and then when the benefits outweigh the drawbacks which in this case seem to me to be mostly about being afraid to color outside the lines.

    It doesn't seem to me, from reading this thread, that it's impossible to implement the feature. I've just started using TS, but have already come across many situations where the lack of it has made me write ugly code that is felt as a hindrance throughout my project.

    TS is great. The lack of true method overloading is not.

    I am sure the core team itself believes the current implementation isn't the best since you practically don't see any documentation of it. Nevermind.

  24. idchlife commented on Apr 28, 2017

    @idchlife

    So, basically current overloading simplified is like:

    class A {
      overloaded(a: string | number) {
        if (typeof a === "string") {
          console.log("Got string");
        } else if (typeof a === "number") {
          console.log("Got number");
        }
      }
    }
    
    const a = new A();
    
    a.overloaded(2);
    
    a.overloaded("something");

    I mean this example I wrote is event easier to write instead of many methods with different parameters and one method in the end with one parameter.
    Well, when there are multiple parameters, that's the case, but... Solved by default value of parameter and type aliases.

    So basically no overloading at all. Just combined types and developer should write by hand overloading functionality everytime (if this than that)

    Am I right?

    Further into discussion of ONE TRUE MORTY OVERLOADING

    TypeScript denied requests about runtime checks etc, because TS is about compile-time type checking and functionality.

    True overloading is not possible because TypeScript would have to add runtime check into compiled code. It's this story again - no runtime checks.
    TS have different philosophy, you guys!

    Maybe someone can come up with idea, that is compatible with TypeScript and can give us functionality of overloading? Like something with decorators maybe, generics, etc?

  25. RyanCavanaugh commented on Apr 28, 2017

    @RyanCavanaugh
    Member

    Realistically, JavaScript does not have function overloading and in general I advise people to just not use overloading at all. Go ahead and use union types on parameters, but if you have multiple distinct behavioral entry points, or return types that differ based on inputs, use two different functions! It ends up being clearer for callers and easier for you to write.

  26. idchlife commented on Apr 28, 2017

    @idchlife

    Ryan Cavanaugh (@RyanCavanaugh) right now I stumbled upon this situation. I can have my object from container via generic get(class) or get(name: string). Container has 2 registrations: by class and by name.

    The thing is it took too much effort to force this overloading work and I already missing one feature - generic with overloading filled with if else does not return type that was provided in argument but it's default generic type (used in <> in class).

    Anyway, long story short: get(), and getByName() right now.

    But!

    Libraries like this are working nicely with overloading: https://github.com/typeorm/typeorm/blob/master/src/entity-manager/EntityManager.ts#L59

    What you are talking about is (if I'm not mistaken): do not use TypeScript overloading.

    But some people find it very helpful, as we can learn from library I mentioned.

  27. rhyek commented on Apr 28, 2017

    @rhyek

    Ryan Cavanaugh (@RyanCavanaugh) :

    use two different functions! It ends up being clearer for callers and easier for you to write.

    I guess you're right! I should stop using function overloading in Java and C# entirely and go with this approach because they are just too impractical!

  28. RyanCavanaugh commented on Apr 28, 2017

    @RyanCavanaugh
    Member

    What's the sarcasm for? We can and should apply different heuristics for environments with compile-time method resolution vs those without.

  29. Shlomibo commented on Jun 21, 2017

    @Shlomibo
    Author

    Ryan Cavanaugh (@RyanCavanaugh)

    Realistically, JavaScript does not have function overloading and in general I advise people to just not use overloading at all. Go ahead and use union types on parameters, but if you have multiple distinct behavioral entry points, or return types that differ based on inputs, use two different functions! It ends up being clearer for callers and easier for you to write

    But, TS suppose to support common patterns in JS, while helping to eliminate common bugs.
    And overloading multiple functionalities into single function is still common in JS, and error prone (if you'll forget to call or implement an "overload", nothing would tell you that).
    That's the reason TS had to have some support of functions overloading in the first place.

    My suggestion also helps in partitioning such overload into different functions, making the code clearer.
    It helps to achieve exactly what you say is better (and I tend to agree) with function overloading.

  30. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions