microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.74k stars 12.46k forks source link

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

Closed Shlomibo closed 7 years ago

Shlomibo commented 7 years ago

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.

HerringtonDarkholme commented 7 years ago

Possible duplicate of https://github.com/Microsoft/TypeScript/issues/3442. IMHO, compile time generated function is possible contrary to TS's design philosophy

Shlomibo commented 7 years ago

@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.

aluanhaddad commented 7 years ago

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?

Shlomibo commented 7 years ago

@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.

froh commented 7 years ago

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.

Shlomibo commented 7 years ago

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

froh commented 7 years ago

@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.

Shlomibo commented 7 years ago

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?

avchugaev commented 7 years ago

Ideas of class methods overloading implementation:

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# ;).

avchugaev commented 7 years ago

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.

aluanhaddad commented 7 years ago

@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.

@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.

avchugaev commented 7 years ago

@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.

aluanhaddad commented 7 years ago

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.

avchugaev commented 7 years ago

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?

aluanhaddad commented 7 years ago

It is not half way if that is an explicit non goal: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals Also goals prioritize alignment with JavaScript https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#goals

froh commented 7 years ago

If I'm not mistaken the value of function overloading is to simplify generic code in complex systems? No need for manual dynamic type dispatch on function arguments because the code for that is generated? So the source code that humans reason about and maintain is simpler, because it is generic.

I have maintained cobol code that required to add new business objects to dispatch tables (if cascades) manually. That was no fun.

So I find function overloading nicely matches goal 2 (structuring for larger pieces of code), and imnsho also non-goal 5 (run-time metadata? nope. encourage statically typed and dispatched code).

at some point you hesitate flattening of types is simple or even feasible (per my suggestion to use a standardized hash of a standardized flattening for mangling) --- well it's about as complex as the types that happen to occur in the program, and it's non-trivial but certainly simple enough to be feasible.

I'd even try to spell it out if I knew it's not for /dev/null.

Shlomibo commented 7 years ago

@achugaev93 I think that exposing each overload and resolving the call statically - must be limited to intra-module calls. Any exposed/public name-mangled function/method would impose huge limitations on javascript projects dependent of the code, which cannot infer which implementation serves which overload, and versioning (it makes it too easy to require any dependent code to recompile, not to mention, again, JS consumers).

About compiler-generated functions that would resolve the overload dynamically, I don't think it's mutually-excluded with user-generated such functions.

An early implementation could adapt my suggestion, and later define cases where the compiler might generate such function, and let programmer omit the entry-function on such cases. Anyway, IMHO, it would still be worth the better compile-time checkings for overloaded functions, and the better separation of concerns.

mhegazy commented 7 years ago

This has been already discussed in #3442. I would recommend keeping the discussion in one thread.

As mentioned in https://github.com/Microsoft/TypeScript/issues/3442#issuecomment-223773471, this is currently out of scope of the TS project.

Shlomibo commented 7 years ago

@mhegazy This is different from that suggestion. And IMHO aligns with the 1-5 of the design goals of TS

froh commented 7 years ago

@mhegazy it's different from #3442 . That is suggesting dynamic type checks of the caller arguments signature to dispatch to the right function. Here there is the proposal to select the signature to call statically (e.g. via a well defined compact hash on the argument type signature). That is a major difference in the readability of the emitted code. As long as the hashing generates a reasonably short hash, the emitted code will continue to be human-readable, intelligible and thus compliant to both the TS goals and non-goals.

I'm not sure what the best format on github would be to give this pro and con discussion a better format than the forth and back here that is growing longer and longer. we need to juxtapose the pros and cons of having or not having function overloads. and we need to juxtapose implementation alternatives. Do we use some google drive like document or for heavens sake something wiki-ish, where we could have such sections "pro/con overloads", and "implementation alternatives" and "pro/con alternative 1, 2, 3" ?

aluanhaddad commented 7 years ago

@froh #3442 explicitly states that it would be resolved statically.

Shlomibo commented 7 years ago

@RyanCavanaugh I believe this suggestion can solve the problems pointed out in these issues and few more https://github.com/Microsoft/TypeScript/issues/13235 https://github.com/Microsoft/TypeScript/issues/13225 While remaining faithful to the goals of TS. Would you consider looking at it?

Thank you very much

Shlomibo commented 7 years ago

@aluanhaddad Being dynamically executed, and exposed as a single function which can be consumed like all JS functions, from any JS code, and without ugly decorations (make the mangling whatever you want. it would still make the calls ugly) that may change from one version of the code to another - is exactly what makes this suggestion different from the others, and in the scope of TS design goals.

I actually oppose any static calls between modules. I believe static calls should be limited to the module itself - making them so limited that I wouldn't bother implement them beyond the overloaded function itself.

mhegazy commented 7 years ago

@mhegazy it's different from #3442 . That is suggesting dynamic type checks of the caller arguments signature to dispatch to the right function. Here there is the proposal to select the signature to call statically (e.g. via a well defined compact hash on the argument type signature). That is a major difference in the readability of the emitted code. As long as the hashing generates a reasonably short hash, the emitted code will continue to be human-readable, intelligible and thus compliant to both the TS goals and non-goals.

I am assuming this proposal is not meant for overloads that only target primitives like number and string. So the emitted code needs to handle a full range of inputs, i would assume something like:

Now, the complexity of detecting structural types a side, this would be type directed emit, and it would violate one of the TS design goals, namely 8, and 3 (and though subjective 4).

This also breaks one main TS scenario which is isolated module transpilation (i.e. transpiling a single file without loading the whole program).

This proposal is different in details from the one in #3442, but it runs against the same constraints.

The issue of overloads has been one that was discussed in length in the early days of TypeScript; given the constraints we are working within, and the familiarity of JS developers with dynamically checking types of inputs to determine the intended function behavior, the current design seemed like the right place tom be.

Shlomibo commented 7 years ago

@mhegazy

This proposal is different in details from the one in #3442, but it runs against the same constraints

How come? Please elaborate on that.

The emitted code is almost identical to the written code (there's no really code generation). Also, this proposal does not handle the selection and dispatch of the correct overload (beside static calls between overloads - which does not affect modularity): therefore the emitted code needs not handle any of these scenarios.

Did you read it? I think you are confusing this proposal with some other one.

HerringtonDarkholme commented 7 years ago

Now, the complexity of detecting structural types a side, this would be type directed emit, and it would violate one of the TS design goals, namely 8, and 3.

type directed emit means, your generated code is dependent on the type annotation.

That is, to put it concrete,

function overload(a: string) { ... } // mark 1
function overload(b: number) { ... }  // mark 2

generates

function overload(arg) {
  function overload$string { // this depends on `string` annotation on mark1
   ...
  }
  function overload$number { // this depends on `number` annotation on mark2
  }
}

Which effectively means, if your code changes, only in type annotation, to

function overload(a: boolean) { ... } // mark 1
function overload(b: number) { ... }  // mark 2

generates

function overload(arg) {
  function overload$boolean { // the generated code changes solely because type on mark 1 changes
   ...
  }
  function overload$number { // this depends on `number` annotation on mark2
  }
}

In case English is not your native language, you can read directed's definition. And dependent here

Shlomibo commented 7 years ago

@HerringtonDarkholme False. Because implementation are enclosed inside the entry-overload (which means they don't clutter the namespace/scope) - you could name them however you'd like.

They could be named imp_1(...) imp_2(...) etc' regardless of name of the overloaded function, nor the implementations` params` types.

HerringtonDarkholme commented 7 years ago

function overload(arg) {

    return typeof arg === 'number'
        ? overload(arg)
        : overload(arg);
}
function overload(num: number): string {
}
function overload(str: string): string{
}

produces


function overload(arg) {

    return typeof arg === 'number'
        ? overload1(arg)
        : overload2(arg);
/// omitted
}

Now, change it to


function overload(arg) {

    return typeof arg === 'string'
        ? overload(arg)
        : overload(arg);
}
function overload(num: number): string {
}
function overload(str: string): string{
}

function overload(arg) {

    return typeof arg === 'string'
        ? overload2(arg) // changed solely because arg's type is changed
        : overload1(arg);
/// omitted
}
Shlomibo commented 7 years ago

@HerringtonDarkholme It is an example of how emitted code could look like. It is not dependent on a specific kind of name mangling. Only on that that names must be mangled because the name cannot be shared between overloads. I'll clear it in the suggestion.

Moreover, because the emitted names are not shared between modules, changes in types cannot break code (I believe this is the reason for avoiding type directed emits) I think it is not such a demerit in this case. Anyway, that besides the point:

you could name them however you'd like.

HerringtonDarkholme commented 7 years ago

More weird usage that is accepted by current compiler

function overload(arg) {
  return overload(<any>arg) // what should this emit?
}
function overload(num: number): string {
}
function overload(str: string): string{
}

Recursion:

function forEachRecurse(arrayOrObj: any[] | any) {
  if (isArray(arrayOrObj)) {
     forEachRecurse(arrayOrObj)
  } else {
    forEachRecurse(arrayOrObj) // hmm, any is of course assignable to any[]
  }
}
function forEachRecurse(arrayOrObj: any[]) {
  for (let item of arrayOrObj) 
     forEachRecurse(item) // what should this overload?
  if (Math.random() > 0.5) forEachRecurse(arrayOrObj) // what about this?
}
function forEachRecurse(arrayOrObj: any) {
  console.log(arrayOrObj) 
}

callback function, a pure type directed example

function callLater(cb: (str) => void | (str, str2) => void) {
   if (someMagic(cb)) { // possibly fn.length, easily broken by uglify js 
     callLater(<(str) => void>cb) // runtime semantic depends on compile time resolution
   } else {
      callLater(<(str, str2) => void>cb) // type only directed
   }
}

Another pure type directed example: type brand, valid technique in current compiler

type Mile = number & {__mileBrand: never}
type KM = number & {__kmBrand: never}

function convertToMeter(distance: Mile | KM, context) {
  return isMileContext(context)) ? convertToMeter(<Mile>distance, context) : convertToMeter(<KM>distance, context); 
function convertToMeter(distance: Mile, context) {
  return distance * 1609.34
}
function convertToMeter(distance: KM, context) {
  return distance * 1000
}
Shlomibo commented 7 years ago
function overload(arg) {
  return overload(<any>arg) // what should this emit?
}
function overload(num: number): string {
}
function overload(str: string): string{
}

Calls between overloads resolved statically. So

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

In order for the compiler to pick the correct typecheck, it follows a similar process to the underlying JavaScript. It looks at the overload list, and proceeding with the first overload attempts to call the function with the provided parameters.

And actually, adding an error in case of ambiguity should be easy.


Please elaborate on what exactly is challenging in the recursive call?

aluanhaddad commented 7 years ago

Moreover, because the emitted names are not shared between modules, changes in types cannot break code (I believe this is the reason for avoiding type directed emits) I think it is not such a demerit in this case. Anyway, that besides the point:

On what do you base this assumption?

Shlomibo commented 7 years ago

@aluanhaddad It's beside the point.

But if it's really important to you you can

  1. Assume that if the suggestion would be picked up - it would be implemented otherwise.
  2. Provide an example of how code that cannot be shared outside of a module (actually, outside a function in a module), can break code in other modules?
HerringtonDarkholme commented 7 years ago

an any can break the emitting schema already. Recursion is just a common pattern that uses this. For example, a flatten function that collapse nested array.

function flatten(a: any[] | any) {}
function flatten(a: any[])
function flatten(a: any)

It looks at the overload list, and proceeding with the first overload attempts to call the function with the provided parameters.

Because any can be assigned to any value and vice versa, compiler will always pick up the first overload, flatten(a: any[]) this case. generates

function flatten(a) {
  if (isArray(a)) flatten$array(a)
  else flatten$array(a) // expected flatten$any
}
Shlomibo commented 7 years ago

@HerringtonDarkholme Good point, yet it can be decided that static resolution would happen only in the entry-overload, leaving all other calls to resolve dynamically.

Add an ambiguity error (for ambiguous calls in the enrty-overload), and your done.

aluanhaddad commented 7 years ago

It's beside the point.

I do not understand.

It is very much to the point as it clearly violates Goal 9. Use a consistent, fully erasable, structural type system.

Assume that if the suggestion would be picked up - it would be implemented otherwise.

Type directed emit is type directed emit. See https://github.com/Microsoft/TypeScript/issues/12041#issuecomment-272817803

Provide an example of how code that cannot be shared outside of a module (actually, outside a function in a module), can break code in other modules?

You are presuming that encapsulation is the sole reason or the even primary reason that type directed emit is contrary to the TypeScript's design goals. On what do you base this assumption?

avchugaev commented 7 years ago

I think compile should at least warn (or better emit error) is it can resolve type of arguments / return value. This means that overloaded methods should be strongly typed (without usage of any type or any "flexible" types definitions).

aluanhaddad commented 7 years ago

@achugaev93 ~--noImplicitAny~ Oh I see, you are suggesting that use of explicit any on overload implementation be disallowed in order to pave the way for type directed emit.

HerringtonDarkholme commented 7 years ago

Add an ambiguity error

What is ambiguity? What about this?

interface CanMove {move: Function}
class Cat {
  // move() {} // toggle comment on this line
}
function overload(a: CanMove | Cat) {
   if (a instanceof Cat) {
     overload(a) // overload$CanMove or overload$Cat ? 
   }
}
function overload(a: CanMove)
function overload(a: Cat)
avchugaev commented 7 years ago

@aluanhaddad yep

Shlomibo commented 7 years ago

@aluanhaddad It is beside the point, as the proposal is not dependent by any mean on a specific mangling. As I said before:

you could name them however you'd like.

Is it so hard for you to accept?

I'm not assuming anything, but that this is an unrelated issue. They could could be named imp_1, imp_2... etc. They could could be named \<overloadname>_TS_AWESOME, \<overloadname>_TS_GREAT, etc from a list of 100 adjectives. They could be named as a UUID.

They could be named however the design team would see appropriate.

@HerringtonDarkholme Have no idea... I wonder if the man who created C# solved such issues.

Can I write code like

static int X(string s) {... }
static int X(object o){...}
...
X("abc");

Are there solid and deterministic rules to rank and distinguish between overloads? To check type compatibility of arguments? Is a class more specific than interface?

avchugaev commented 7 years ago

@HerringtonDarkholme overloaded functions must be defined for specific type. This means you should not use types like CanMove | Cat.

aluanhaddad commented 7 years ago

@Shlomibo

Have no idea... I wonder if the man who created C# solved such issues.

Can I write code like

static int X(string s) {... } static int X(object o){...} ... X("abc"); Are there solid and deterministic rules to rank and distinguish between overloads? To check type compatibility of arguments? Is a class more specific than interface?

Indeed he did. See https://github.com/Microsoft/TypeScript/issues/12041#issuecomment-271210233

avchugaev commented 7 years ago

compiler should select method with closest type:

Shlomibo commented 7 years ago

@achugaev93 And whenever there is no obvious preference... It would be an ambiguity error :)

Shlomibo commented 7 years ago

@aluanhaddad It is not a model for TS. It can model overloads in this context. (as you can surely notice, these would not be C# overloads... what is an entry-overload in C#?!)

aluanhaddad commented 7 years ago

compiler should select method with closest type:

class that match argument type super class that match argument type interface. Correct me if I'm wrong.

Different language, different runtime, different rules.

And whenever there is no obvious preference... It would be an ambiguity error :)

Introducing a massive breaking change by turning the long accepted method of describing a function that can take different sets of arguments at runtime is a non-starter.

avchugaev commented 7 years ago

Different language, different runtime, different rules.

It would be greate typescript follow general OOP rules. Other languages already passed this way successully. Why TypeScript can't?

Shlomibo commented 7 years ago

@aluanhaddad

Introducing a massive breaking change by turning the long accepted method of describing a function that can take different sets of arguments at runtime is a non-starter

No it is not. It is introduced with new syntax. Meaning any valid program of today is not using that syntax, and therefore cannot break by this syntax rules

aluanhaddad commented 7 years ago

It would be greate typescript follow general OOP rules. Other languages already passed this way successully. Why TypeScript can't?

Because TypeScript is a superset of JavaScript targeting JavaScript Virtual Machine implementations, such as V8 and Chakra, and aims to not emit semantically different JavaScript based on the static type annotations it adds to the language. TypeScript was designed to align precisely with ECMAScript, both behaviorally and idiomatically, in the value space. In other words this was a design decision.