While each of those flexibilities can be useful, they interact in annoying ways. The fact that map passes three arguments but is often used as if it was passing one, and that parseInt accepts two arguments but is often used as if it accepted one makes it very easy to make this mistake.
This just goes to show how one can use a language without actually understanding its core semantics. Additional arguments aren't "discarded" at all: they're still available to the called function in via `arguments`, they're just not required to be assigned a name in the function signature.
Basically, a Javascript function fn() declaration is equivalent to Python's def fn(*args): declaration and you have the option to assign a name to positional arguments. Any named positional argument that is not provided by the caller simply leaves the argument uninitialized, i.e. undefined.
It's a core concept of the language and I'm always puzzled when non-beginners struggle with this. That's also a very good reason to not use vanilla Javascript at all and skip straight to TypeScript and its brethren instead.
This issue is all about the number of arguments problem, and TypeScript (in cases like this) won't flag that problem. Which is something I think it should.
If you manually unroll the "map" and call parseInt() explicitly with the same arguments that map() calls parseInt() with, TypeScript will flag that. But not when map() is in the picture.
I understand why they chose to do that, but I still disagree with it.
error TS2345: Argument of type '(string: string, radix?: number) => number' is not assignable to parameter of type '(value: string, index: number, array: string[]) => number'. Target signature provides too few arguments. Expected 3 , but got 2.
console.log(["1","2","3"].map(parseInt));
I'm curious - have you been using this for long? Have you noticed any code which this flagged and which you thought it was being too strict? I feel like I would want this flag on all of the time, but I'm curious if there are edge cases that I have not considered but maybe you have experienced.
If I do not want to change existing code to add parameters for each callback I use trick bellow : type JavaScriptCallback< T extends (...args: any) => any, P = Parameters<T> > = P extends [...infer Rest, infer _Last] ? ((...args: Rest) => ReturnType<T>) | JavaScriptCallback<T, Rest> : T;
interface Array<T> { forEach( callbackfn: JavaScriptCallback<(value: T, index: number, array: T[]) => void>, thisArg?: any ): void;
map<U>(
callbackfn: JavaScriptCallback<(value: T, index: number, array: T[]) => U>,
thisArg?: any
): U[];
}
and then is more like standard TypeScript would not complain about
parseInt because I redefined typedef of map
to accept 0 or 1 or 2 or 3 parameters .
But I am in control.
Only edge cases in some callbacks I notice tsc complains that type is any with strict option turn on
then I add a type .
It is experimental, would prefer if they add it as option.
Change is just in checker emitted JavaScript is still same.
As always there are some trades of.
But for me it works so far so good ;-)No other language does this.
function fn() { }
is semantically the exact same as function fn(a, b, c) { }
in JS.JS has this semantic and the interfaces are designed with that in mind.
Take C# for example. There is a List<T>.ForEach() function that passes the value to the delegate. Fine. But what if I require the index as well? I can't use List<T>.ForEach() in that case, because ignoring extra arguments is not part of C# function call semantics.
The interface of map() has been designed to be as flexible as possible to cover use cases outside of just passing the current value. This matches perfectly with JS function call semantics that allow any number of arguments by default.
Why doesn't parseInt() *always* require two parameters? Why is one design decision "strange and unique" (e.g. map() passing 3 arguments) while the other (parseInt() accepting one or two arguments) is perfectly fine? I also would be careful with citing the STL as being sound when it comes its interface design :)
JS is different from other languages - big whoop. The same complaints about seemingly strange design decisions can be made for any sufficiently old and widespread language - JS isn't unique in this regard.
There actually annoying strangeness in other things, e.g. confusing type conversions in places (e.g. ("" == false) === true; [1, 2, 3] + [4, 5, 6] === "1,2,34,5,6"), which is why "==" should never be used if you want to retain your sanity.
The interfaces and standard functions, however, are as offensive or inoffensive as those in most other languages.
More specifically, you could in C89 but it was removed because it's a bad feature.
Every language that supports default and variable arguments does this. This includes Python, C, and C++ and has absolutely zero to do with "loose typing" in this case.
> help(int)
class int(object)
| int([x]) -> integer
| int(x, base=10) -> integer
> list(map(int, ["1", "2", "3"]))
[1, 2, 3]
Even if you define a function that takes two parameters, it complains about not having a second one: >>> def to_int(a, b):
... return int(a, base=b)
...
>>> list(map(to_int, ["1", "2", "3"]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: to_int() missing 1 required positional argument: 'b' def to_int(a, b):
...
with the JS semantic equivalent of def to_int(*args):
...
because that's how JS function declarations work. You simply have the option to assign names to positional parameters, i.e. function fn(a, b) {
comnsole.log(a, b)
}
is just syntactic sugar for function fn() {
const a = arguments[0]
const b = arguments[1]
console.log(a, b)
}and that's what people seem to struggle with and argument over for whatever strange reason.
But you can't!!!
> Every language that supports default and variable arguments does this.
Python:
map(float, ["1", "2", "3"])
C:No equivalent syntax.
C++
std::vector<std::string> a = {"1", "2", "3"};
std::vector<int> b;
std::transform(a.begin(), a.end(), std::back_inserter(b), std::stod); # compile error
You can say "well feature X exists elsewhere" or "library function Y exists elsewhere", but only JS makes the collective design choices that cause this phenomenon.Good, bad? IDK, that's subjective. But unique? Certainly.
If you ever, like at all, had the pleasure of using any Python library without type hints - past iterations of numpy and in particular matplotlib come to mind - you'd be blown away by the amount of
def some_function(*args, **kwargs)
Have fun trying to figure those out even with an API reference at hand. Fun times.Also C does have equivalent syntax, namely variadic functions in combination with function pointer arguments. You can design all kinds of crazy interfaces with that, too, which will exhibit strange and unexpected behaviours if used incorrectly. Heck, name a single C noob who didn't cause a segfault while trying to read a number using sscanf().
When it comes to stdlib interface design, C is actually much more offensive than JS :) strtok() comes to mind. More footguns than stars in the sky just in that innocent seeming function alone. And don't even get me started with the C++ STL...
So no, it's not just JS that makes design choices that seem odd and unintuitive - you'll find them in every language that is actually in widespread use and "battle tested". It's funny to me how some people try to single out JS in that regard, even though it's in no way special when it comes to this.