add({a:2, b:4})
function add({a, b}) { return a + b }
function _add({a, b, multiplier = 1}) { return a+b*multiplier }
challenge: come up with a breaking use that does not involve something having a property named "multiplier"
add({a:2, b:4})
function add({a, b}) { return a + b }
function _add({a, b, multiplier = 1}) { return a+b*multiplier }
challenge: come up with a breaking use that does not involve something having a property named "multiplier"
However, let's keep the tension for while. The fact that having a working function using "multiplier" is already a bit broken - it shouldn't work from the beginning, but javascript is designed so that it does. Hence you have to give me this restriction, because otherwise it is obvious how your example can be "broken".
Before I expose my (very simple) solution to still break your example even without using "multiplier", I would like to ask you to try to come up with another example first, where you don't require restrictions. I think it's a good exercise. :)
If no one comes up with one, I'll show it in, say, a day from now.
console.log(add({a:"1", b:2})) //12
console.log(_add({a:"1", b:2})) //21
So to make the code break with the change to _add, I can just do: if(add({a:"1", b:2}) != 12) boom()For day to day, I feel this pattern is good enough, as typescript works nicely with it.
A tricky thing I could see without ever explicitly defining "multiplier" (e.g. on the object prototype) is passing a Proxy that e.g. has a fallback for all missing properties. Detecting a proxy is only kind of possible (?) but we can copy all the original target properties from it, which should make it safe.
So here goes, my safe solution for modifying function signature in a non breaking way:
function add({ ...args }) {
const { a, b, ...rest } = args;
if (typeof a !== "number" || typeof b !== "number") {
throw "all arguments must be numbers";
}
if (Object.keys(rest).length > 0) {
throw "You may only pass arguments a and b";
}
return a + b;
}
function _add({ ...args }) {
const { a, b, multiplier = 1, ...rest } = args;
if (
typeof a !== "number" ||
typeof b !== "number" ||
typeof multiplier !== "number"
) {
throw "all arguments must be numbers";
}
if (Object.keys(rest).length > 0) {
throw "You may only pass arguments a, b and multiplier";
}
return a + b * multiplier;
}
Most of these issues (not the proxy one) should be solved by typescript.In the evil world, I can break your code like that:
try {
add(1, 2, 3, 4)
} catch (e) {
if(e !== "You may only pass arguments a and b")
throw "boom";
}
However, you can of course make your exception string generic.Then I'll have no choice to use one of my jokers: calling "add.toString()" and inspect your function in detail. Before you scream that this is stupid, please mind that this is actually used out there (looking for example at you, angular).
function add({ ...args }) {
const { a, b, ...rest } = args;
if (typeof a !== "number" || typeof b !== "number") {
throw "no";
}
if (Object.keys(rest).length > 0) {
throw "no";
}
return a + b;
}
function _add({ ...args }) {
const { a, b, multiplier = 1, ...rest } = args;
if (
typeof a !== "number" ||
typeof b !== "number" ||
typeof multiplier !== "number"
) {
throw "no";
}
if (Object.keys(rest).length > 0) {
throw "no";
}
return a + b * multiplier;
}
add.toString = () => "nice try";
_add.toString = () => "nice try";
Edit: OK I think we are stretching HN comment ettiquete to far with this much code. This was fun though. Thanks. if( Function.prototype.toString.call(add).includes("multiplier") ) throw "boom!";
> Edit: OK I think we are stretching HN comment ettiquete to far with this much code. This was fun though. Thanks.Huh? Would you mind to educate me about what part of the ettiquete we are not following?
At this point we can go ahead and break the world:
add.toString = () => `function add({ ...args }) { const { a, b, ...rest } = args; if (typeof a !== "number" || typeof b !== "number") { throw "no"; } if (Object.keys(rest).length > 0) { throw "no";} return a + b;}`;
_add.toString = () => `function add({ ...args }) { const { a, b, ...rest } = args; if (typeof a !== "number" || typeof b !== "number") { throw "no"; } if (Object.keys(rest).length > 0) { throw "no";} return a + b;}`;
Function.prototype.toString = () => `function add({ ...args }) { const { a, b, ...rest } = args; if (typeof a !== "number" || typeof b !== "number") { throw "no"; } if (Object.keys(rest).length > 0) { throw "no";} return a + b;}`;Now, we are leaving the original scope (not just changing a function, but modifying globals). read-only globals even. But prepare for my counter:
let frame = document.createElement('x');
document.body.appendChild(frame);
if( frame.contentWindow.Function.toString.call(add).includes("multiplier") ) throw "boom!";
You might go to also kill "document.createElement", but there are many ways for me to get a new frame. I think when we come to the point where all these are disabled, I would say only a small fraction of the websites that use javascript would still properly operate. It would be your victory though. ;)That is stupid though. It's like saying changing a private field in Java is a breaking change because someone might have used reflection to access it.
Taken to the moronic extreme: any detectable change is a breaking change because someone could write a function that pulls your latest release and depends on every bit being identical with the previous release.
> It's like saying changing a private field in Java is a breaking change because someone might have used reflection to access it.
Which is true, both in theory and practice.
Even look at misc.Unsafe - which is deliberately named unsafe and everyone was told not to use it. Then they tried to drop support for it and people freaked out so much that support was continued. (https://jaxenter.com/java-9-without-sun-misc-unsafe-119026.h...)
> Taken to the moronic extreme: any detectable change is a breaking change because someone could write a function that pulls your latest release and depends on every bit being identical with the previous release.
I would say it is best described here: https://xkcd.com/1172/