Function overloading in Javascript - Best practices

Viewed 551083

What is the best way(s) to fake function overloading in Javascript?

I know it is not possible to overload functions in Javascript as in other languages. If I needed a function with two uses foo(x) and foo(x,y,z) which is the best / preferred way:

  1. Using different names in the first place
  2. Using optional arguments like y = y || 'default'
  3. Using number of arguments
  4. Checking types of arguments
  5. Or how?
37 Answers

The best way to do function overloading with parameters is not to check the argument length or the types; checking the types will just make your code slow and you have the fun of Arrays, nulls, Objects, etc.

What most developers do is tack on an object as the last argument to their methods. This object can hold anything.

function foo(a, b, opts) {
  // ...
  if (opts['test']) { } //if test param exists, do something.. 
}


foo(1, 2, {"method":"add"});
foo(3, 4, {"test":"equals", "bar":"tree"});

Then you can handle it anyway you want in your method. [Switch, if-else, etc.]

There is no real function overloading in JavaScript since it allows to pass any number of parameters of any type. You have to check inside the function how many arguments have been passed and what type they are.

There are two ways you could approach this better:

  1. Pass a dictionary (associative array) if you want to leave a lot of flexibility

  2. Take an object as the argument and use prototype based inheritance to add flexibility.

The best way really depends on the function and the arguments. Each of your options is a good idea in different situations. I generally try these in the following order until one of them works:

  1. Using optional arguments like y = y || 'default'. This is convenient if you can do it, but it may not always work practically, e.g. when 0/null/undefined would be a valid argument.

  2. Using number of arguments. Similar to the last option but may work when #1 doesn't work.

  3. Checking types of arguments. This can work in some cases where the number of arguments is the same. If you can't reliably determine the types, you may need to use different names.

  4. Using different names in the first place. You may need to do this if the other options won't work, aren't practical, or for consistency with other related functions.

Not everybody knows that you can do Destructuring assignment directly in a function signature.

Thanks to that you can easily define a very flexible method signature, which is, IMHO, superior to Java method overloading.

Example:

const myFunction = (({a, b, c}) => {
    console.log(a, b, c);
});

myFunction({a: 1, b: 2});
myFunction({a: 1, b: 2, c: 3});

You don't even need to respect the order of the parameters, and there is naming consistency between the calling statement and the target method signature.

You can also assign default values:

const myFunction = (({a = 1, b = 2, c} = {}) => {
    console.log(a, b, c);
});

check this out. It is very cool. http://ejohn.org/blog/javascript-method-overloading/ Trick Javascript to allow you to do calls like this:

var users = new Users();
users.find(); // Finds all
users.find("John"); // Finds users by name
users.find("John", "Resig"); // Finds users by first and last name

You can now do function overloading in ECMAScript 2018 without polyfills, checking var length/type, etc., just use the spread syntax.

function foo(var1, var2, opts){
  // set default values for parameters
  const defaultOpts = {
    a: [1,2,3],
    b: true,
    c: 0.3289,
    d: "str",
  }
  // merge default and passed-in parameters
  // defaultOpts must go first!
  const mergedOpts = {...defaultOpts, ...opts};

  // you can now refer to parameters like b as mergedOpts.b,
  // or just assign mergedOpts.b to b
  console.log(mergedOpts.a);
  console.log(mergedOpts.b);
  console.log(mergedOpts.c);  
  console.log(mergedOpts.d);
}
// the parameters you passed in override the default ones
// all JS types are supported: primitives, objects, arrays, functions, etc.
let var1, var2="random var";
foo(var1, var2, {a: [1,2], d: "differentString"});

// parameter values inside foo:
//a: [1,2]
//b: true
//c: 0.3289
//d: "differentString"

What is spread syntax?

The Rest/Spread Properties for ECMAScript proposal (stage 4) adds spread properties to object literals. It copies own enumerable properties from a provided object onto a new object. More on mdn

Note: spread syntax in object literals doesn't work in Edge and IE and it is an experimental feature. see browser compatability

there is no actual overloading in JS, anyway we still can simulate method overloading in several ways:

method #1: use object

function test(x,options){
  if("a" in options)doSomething();
  else if("b" in options)doSomethingElse();
}
test("ok",{a:1});
test("ok",{b:"string"});

method #2: use rest (spread) parameters

function test(x,...p){
 if(p[2])console.log("3 params passed"); //or if(typeof p[2]=="string")
else if (p[1])console.log("2 params passed");
else console.log("1 param passed");
}

method #3: use undefined

function test(x, y, z){
 if(typeof(z)=="undefined")doSomething();
}

method #4: type checking

function test(x){
 if(typeof(x)=="string")console.log("a string passed")
 else ...
}

Something like this can be done for function overloading.

function addCSS(el, prop, val) {
  return {
    2: function() {
      // when two arguments are set
      // now prop is an oject
      for (var i in prop) {
          el.style[i] = prop[i];
      }
    },
    3: function() {
      // when three arguments are set
      el.style[prop] = val;
    }
    }[arguments.length]();
}
// usage
var el = document.getElementById("demo");
addCSS(el, "color", "blue");
addCSS(el, {
    "backgroundColor": "black",
  "padding": "10px"
});

Source

Function overloading in Javascript:

Function overloading is the ability of a programming language to create multiple functions of the same name with different implementations. when an overloaded function is called it will run function a specific implementation of that function appropriate to the context of the call. This context is usually the amount of arguments is receives, and it allows one function call to behave differently depending on context.

Javascript doesn't have built-in function overloading. However, this behaviour can be emulated in many ways. Here is a convenient simple one:

function sayHi(a, b) {
  console.log('hi there ' + a);
  if (b) { console.log('and ' + b) } // if the parameter is present, execute the block
}

sayHi('Frank', 'Willem');

In scenarios where you don't know how many arguments you will be getting you can use the rest operator which is three dots .... It will convert the remainder of the arguments into an array. Beware of browser compatibilty though. Here is an example:

function foo (a, ...b) {
  console.log(b);
}

foo(1,2,3,4);
foo(1,2);

While Default parameters is not overloading, it might solve some of the issues that developers face in this area. The input is strictly decided by the order, you can not re-order as you wish as in classical overloading:

function transformer(
    firstNumber = 1,
    secondNumber = new Date().getFullYear(),
    transform = function multiply(firstNumber, secondNumber) {
        return firstNumber * secondNumber;
    }
) {
    return transform(firstNumber, secondNumber);
}

console.info(transformer());
console.info(transformer(8));
console.info(transformer(2, 6));
console.info(transformer(undefined, 65));

function add(firstNumber, secondNumber) {
    return firstNumber + secondNumber;
}
console.info(transformer(undefined, undefined, add));
console.info(transformer(3, undefined, add));

Results in (for year 2020):

2020
16160
12
65
2021
2023

More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters

The Typescript Handbook mentions Overloads. While checking types and doing different logic based on results has been mentioned before, this approach defining multiple named functions to work with a typing system might be interesting to readers. Here is how TypeScript achieves creating a function that accepts multiple types of arguments that will direct the function logic to do different things based on incoming arguments and types:

The answer is to supply multiple function types for the same function as a list of overloads. This list is what the compiler will use to resolve function calls. Let’s create a list of overloads that describe what our pickCard accepts and what it returns.

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x: any): any {
  // Check to see if we're working with an object/array
  // if so, they gave us the deck and we'll pick the card
  if (typeof x == "object") {
    let pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;
  }
  // Otherwise just let them pick the card
  else if (typeof x == "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
  }
}

let myDeck = [
  { suit: "diamonds", card: 2 },
  { suit: "spades", card: 10 },
  { suit: "hearts", card: 4 },
];

let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

With this change, the overloads now give us type checked calls to the pickCard function.

In order for the compiler to pick the correct type check, 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. If it finds a match, it picks this overload as the correct overload. For this reason, it’s customary to order overloads from most specific to least specific.

Note that the function pickCard(x): any piece is not part of the overload list, so it only has two overloads: one that takes an object and one that takes a number. Calling pickCard with any other parameter types would cause an error.

For your use case, this is how I would tackle it with ES6 (since it's already the end of 2017):

const foo = (x, y, z) => {
  if (y && z) {
    // Do your foo(x, y, z); functionality
    return output;
  }
  // Do your foo(x); functionality
  return output;
}

You can obviously adapt this to work with any amount of parameters and just change your conditional statements accordingly.

I've been using this function to prettify my overloads for years:

function overload(){
  const fs = arguments, fallback = fs[fs.length - 1];
  return function(){
    const f = fs[arguments.length] || (arguments.length >= fs.length ? fallback : null);
    return f.apply(this, arguments);
  }
}

Demostrated:

function curry1(f){
  return curry2(f, f.length);
}

function curry2(f, minimum){
  return function(...applied){
    if (applied.length >= minimum) {
      return f.apply(this, applied);
    } else {
      return curry2(function(...args){
        return f.apply(this, applied.concat(args));
      }, minimum - applied.length);
    }
  }
}

export const curry = overload(null, curry1, curry2);

Take a look at jQuery's off method:

  function off( types, selector, fn ) {
    var handleObj, type;
    if ( types && types.preventDefault && types.handleObj ) {

        // ( event )  dispatched jQuery.Event
        handleObj = types.handleObj;
        jQuery( types.delegateTarget ).off(
            handleObj.namespace ?
                handleObj.origType + "." + handleObj.namespace :
                handleObj.origType,
            handleObj.selector,
            handleObj.handler
        );
        return this;
    }
    if ( typeof types === "object" ) {

        // ( types-object [, selector] )
        for ( type in types ) {
            this.off( type, selector, types[ type ] );
        }
        return this;
    }
    if ( selector === false || typeof selector === "function" ) {

        // ( types [, fn] )
        fn = selector;
        selector = undefined;
    }
    if ( fn === false ) {
        fn = returnFalse;
    }
    return this.each( function() {
        jQuery.event.remove( this, types, fn, selector );
    } );
  }

Many overloaded functions when optimized for performance are nigh unreadable. You have to decipher the head of the function. This is perhaps faster than using an overload function like I provide; however, it is slower from a human perspective with regard to identifying which overload was called.

I am working on a library that provides class like code capabilities to Javascript, currently it supports constructors, inheritance, methods overload by number of params and by types of params, mixins, statics properties and singleton.

See an example of method overloading using that library:

eutsiv.define('My.Class', {
    constructor: function() {
        this.y = 2;
    },
    x: 3,
    sum: function() {
        return this.x + this.y;
    },
    overloads: {
        value: [
            function() { return this.x + ', ' + this.y },
            function(p1) { this.x = p1; },
            function(p1, p2) { this.x = p1; this.y = p2; }  // will set x and y
        ]
    }
});

var test = new My.Class({ x: 5 });   // create the object
test.value();                        // will return '5, 2'
test.sum();                          // will return 7
test.value(13);                      // will set x to 13
test.value();                        // will return '13, 2'
test.sum();                          // will return 15
test.value(10, 20);                  // will set x to 10 and y to 20
test.value();                        // will return '10, 20'
test.sum();                          // will return 30

Any feedback, bug fixes, docs and tests improves are welcome!

https://github.com/eutsiv/eutsiv.js

Related