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:
- It’s clear from the outside that we’re dealing with an overload
- The independant signatures are visually explicit and clear
- The signatures are external to the implementations; each implementation can simply trust its arguments
- The signatures are programmatically discoverable (if we want)
(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 ...