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.