frAgile Developer

Stop asking me to do bad things.

30 September 2021

Function Overloading in JavaScript

(Don’t Try This At Home)

The normal way to “overload” functions in JavaScript is usually done by counting arguments, possibly with some minimalistic type testing. Like this:

function log(...args) {
  if (args.length === 1 && typeof args[0] === 'string') {
    console.log('we got a string', args[0]);
  } else if (args.length === 1 && typeof args[0] === 'number') {
    console.log('we got a number', args[0]);
  } else {
    console.error('Invalid arguments!');
  }
};

/audible_sigh

Ok. So, that works, but I think it’s really repetive, it makes for long functions where scoping can get inadvertantly intertwined, and it requires a bit of effort to visually parse out the valid signatures. TypeScript can help with this a little, but it still doesn’t support proper overloading, as you still end up with a single function body and a litany of conditionals.

So, here’s what I’ve been tinkering with. What if we had an overload function that mapped type arrays to implementations? What if we could do something like this instead?

let log = overload(
    [String], function(value) {
        console.log('we got a string', value);
    },
    [Number], function(value) {
        console.log('we got a number', value);
    }
);

If we could something like that, we’d accomplish a few things:

(I’m sure there are other amazing benefits as well.)

And, here’s a super basic super naive implementation:

function overload(...overloads) {
    const f = function(...args) {
        let constructorArray = args.map(arg => arg.constructor);
        let implIndex = f.overloads.findIndex(sig => {
            return constructorArray.length === sig.length &&
                constructorArray.every((o,i) => o === sig[i])
            ;
        }) + 1;
        if (implIndex > 0 && typeof(f.overloads[implIndex]) === 'function') {
            return f.overloads[implIndex].apply({}, args);
        } else {
            const message = "There is no implementation that matches the provided arguments.";
            console.error(message, constructorArray);
            throw Error(message);
        }
    };
    f.overloads = overloads;
    return f;
};

Even with this simple, mostly untested, and probably erroneous implementation, we basically have overloading.

> log("abc"); 
we got a string abc

> log(123);
we got a number 123

We can operate on constructed objects too.

class A {
  constructor(value) {
    this.a_value = value;
  }
}

class B {
  constructor(value) {
    this.b_value = value;
  }
}

let log = overload(
    [A], function(o) {
        console.log('we got an A:', o.a_value);
    },
    [B], function(o) {
        console.log('we got a B:', o.b_value);
    }
);

And, as expected, this also works:

> log(new A('my A value'));
we got an A: my A value

> log(new B('my B value'));
we got a B: my B value

What do you think? Does something like this have utility for you? Are there already better overloading implementations out there?

Back to index ...