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.
```C#
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.
```js
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;
}
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.
Possible duplicate of https://github.com/Microsoft/TypeScript/issues/3442.
IMHO, compile time generated function is possible contrary to TS's design philosophy
@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.
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?
@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.
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?
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.
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.
@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?
@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.
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?
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# ;).
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.
@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.
@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.
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.
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?
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
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.
@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.
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.
@mhegazy
This is different from that suggestion.
And IMHO aligns with the 1-5 of the design goals of TS
@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" ?
@froh #3442 explicitly states that it would be resolved statically.
@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
@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 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:
arg === null
for null
typestypeof arg ===
for undefined
, number
, string
, function
, and object
instanceof arg ===
for classes (may be?)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.
@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.
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
@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.
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
}
@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.
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
}
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?
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?
@aluanhaddad
It's beside the point.
But if it's really important to you you can
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
}
@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.
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?
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).
@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.
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)
@aluanhaddad yep
@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 \
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
C#
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?
@HerringtonDarkholme overloaded functions must be defined for specific type. This means you should not use types like CanMove | Cat.
@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
compiler should select method with closest type:
@achugaev93
And whenever there is no obvious preference... It would be an ambiguity error :)
@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#?!)
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.
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?
@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
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.
So make an exception for methods overloading in the design decisions.
Sometimes it's necessary to break rules ;)
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
C#
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.
@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)
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.
The decision is made different proposal, with demerits which I think this proposal working around them
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.
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?
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.
@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.
@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!
What's the sarcasm for? We can and should apply different heuristics for environments with compile-time method resolution vs those without.
@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.
Most helpful comment
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.