Hacker News new | past | comments | ask | show | jobs | submit login
Why [“1”,“2”,“3”].map(parseInt) yields [1, NaN, NaN] in JavaScript (2011) (wirfs-brock.com)
44 points by xg15 on June 23, 2023 | hide | past | favorite | 82 comments



Okay but the whole article could have been summed up in 4 sentences:

> Looking at the specification of parseInt you should notice that it is defined as accepting two arguments. The first argument is the string to be parsed and the second specifics the radix of the number to be parsed. >...the function that is passed as the first argument to map as the callbackfn. The specification says, “the callbackfn is called with three arguments: the value of the element, the index of the element, and the object that is being traversed.”


I read your comment, didn't understand your explanation. Then I read the article and understood. I liked the slower break down in the article.


One paragraph would have been enough, for me, it dragged on and I started skipping sections of text. I am amazed someone wanted to spend their precious hours writing such a long article for something that could have been much less. Different strokes and folks though.


Or two:

> Array.prototype.map passes the element's index as the second argument to the callback, and parseInt accepts an optional second argument as the radix (base).

> Using the index as the radix yields a nonsensical result.


This is a great example of why I wish all function parameters except a first non-optional one always had to be named.

In other words, you could do:

  parseInt("1");
And you could do:

  parseInt("1", radix: 16);
But you could never do:

  parseInt("1", 16);
I swear, this is my #1 wish for programming languages in general. Identifying parameters by position was necessary back when memory was at a premium, but it's always been a disaster for readability and clarity. And we're still all paying the price.

Edit: And I'm not talking about named arguments as an option (lots of languages provide this, though not JavaScript). I mean required, where the very concept of argument order (except for first) doesn't even exist.


That's not the problem. The problem is when a( <a list L> ) becomes a( l[0], l[1], l[2] ), which is bananas. you pass one object, you should stuff that into the first parameter.


I might be misunderstanding you, but if you're saying this is what's happening with `["1","2","3"].map(parseInt)`, that's not correct.

The article describes the situation in detail, but `parseInt` is called three separate times (its first argument in each call is `"1"`, then `"2"`, then `"3"`, as you'd expect with any array-mapping operation). The weirdness arises because `.map` always passes more than one argument to the callback (and `parseInt` accepts more than one parameter). This happens even when the array has only a single element, it's just that the first call (`parseInt("1", 0, ["1"])`) happens to have the same return value as `parseInt("1")`, so you only notice the weirdness with the later elements.


I see! Thank you for clarifying.


It would prevent the bug, though.

Passing the index and object for .map() can come in handy sometimes, so I understand the motivation for including it. But with named arguments, they could be passed as something like "_index:" and "_obj:", rather than getting slotted in as an arbitrary [1] and [2], where they can be misinterpreted as other parameters (like "radix").


I think we're off on a tangent here, but I will at least agree that named parameters are a godsend in any programming language. Things are so much easier to read when the caller can clearly state their intentions for things like "foo(true, false, true)".


if you use visual studio code, i think there is a feature that will give the feature you intend.


> ["1","2","3"].map(function(value) {return parseInt(value)})

> (…) it is much more verbose and arguably less elegant.

Article is from 2011 and referencing ES5. With ES6 arrow functions we can make it shorter and more elegant:

  ["1","2","3"].map(value => parseInt(value))
Because it’s so short, even `n` should suffice and retain clarity:

  ["1","2","3"].map(n => parseInt(n))


Sure, but to update their example using the new format, you'd say:

    ["1","2","3"].map(parseInt); // returns [1, NaN, NaN]
The gotcha trap is still there, and is just as likely to get through a code review.


Yes, you still need to know that you have to do this, but my point is that the author started with the quoted alternative and abandoned it because it had two problems (verbose, inelegant) which are eliminated in ES6, making the `Function.prototype.only` solution unnecessary.


Yep came here to say this.

Arrow functions are graceful and preferred precisely because of this. Passing a naked function reference like that to map is frowned upon where I work, and your PR would not get approval because it is risky for the reasons pointed out in this article.

TL;DR - use modern JavaScript features to make your code more readable and more reliable.


This... I prefer 10x more obvious code than elegant code. Not to mention, altering primitives class prototypes can lead to unexpected side effects.


Spoiler: because `parseInt` accepts the second argument of the radix of the number to parse, and `map` calls the provided callback function with a second argument of the index in the array, so this is the result of calling `[parseInt("1",0), parseInt("2",1), parseInt("3",2)]`

The article is from 2011, and I'm not even sure arrow functions were in the spec at that time. Nowadays the best practice would be: `["1," "2", "3"].map(maybeNum => parseInt(maybeNum))`


The article mentions using the Number constructor, which is even more elegant:

    > ["1","2","3"].map(Number)
    [ 1, 2, 3 ]


Until the Number constructor starts accepting a radix parameter.


This approach seems problematic since values like "", null and false become 0 instead of NaN.


It's concerning the level of handwaving people do by pointing to a specification and showing how this is in accordance with it. This is not a normal way for a language to behave which is why it gets this attention. Languages shouldn't get people to accept their flaws people should push to resolve the flaws in languages.


For better results start at -1 '-1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20'.split(' ').map(parseInt) gets you

    [
      -1, NaN, // could be better
      1,  2,  3,  4,  5,  6,  7,  8,  9, // ok a little bumpy at the start but now we're cookin'
      11,  13, 15, 17, 19, 21, 23, 25, 27, 29, // not great not terrible
      42 // dang
    ]
way closer.

Pretty sure the Number constructor was made to get you around this. map(Number) does what you would expect.


Because you shouldn't try to be clever with Javascript.

Map passes 3 parameter to the mapped function, the second one being an index, and parseInt accepts a second optional argument as radix.

be explicit:

    ["1","2","3"].map(function(x){return parseInt(x,10)})

Javascript isn't the problem here.


TypeScript allows this too, unfortunately. https://www.typescriptlang.org/play?#code/MYewdgziA2CmB00QHM...


Ah, that semi-answers my question - but why? Surely the signature of parseInt doesn't match what map requires for its function parameter?


I agree that it would be nice if it flagged this kind of thing. But TypeScript disagrees.

https://github.com/microsoft/TypeScript/wiki/FAQ#why-are-fun...


Interesting but certainly disagree with the reasoning, that somehow having to explicitly discard the unused parameters would be "burdensome", certainly compared to the need to explicitly declare types for complex arbitrary data structures. If you're not even prepared to write, say, array.map((n, _, _) => parseInt(n)) then why bother using a strictly type checked language...


Agreed on all counts.


The radix param always seemed silly and unnecessary to me. It's a huge footgun that causes a ton of bugs like this. It should have just been another method, "parseIntBase" or whatever.


The problem here is not with parseInt but with map. Map is such a basic function and should have been defined more simply.

By all means, make an extended mapE function that passes all kinds of things like position in the input iterable, a reference to the iterable, a reference to an at-first empty “memo” object that will be discarded at the end of the map call, etc etc.


There are a lot of these poor API design decisions in early JavaScript, which we're stuck with due to the need for backwards compatibility. ("Don't break the web.") Modern JavaScript APIs tend to be pretty good. Fortunately, early JavaScript was also very small, so you mostly memorize the footguns (or use a linter) and you're good. This is one of them.


would a linter help here though? it seems to be a perfectly valid call here, except doing something unintended.


A linter would help, if you have a lint rule that requires a locally defined (regular or arrow) function and not a variable inside of map, filter, etc.


Seems like a very normal parameter choice for such a function. Nearly every one of the "big" programming languages uses this in some way. To me it would seem weird to have a parseInt and parseIntBase when one is just parseInt(x) -> parseIntBase(x, 10)

C++: https://en.cppreference.com/w/cpp/string/basic_string/stol

Java: https://docs.oracle.com/javase/8/docs/api/java/lang/Integer....

C#: https://learn.microsoft.com/en-us/dotnet/api/system.int32.pa...

But they don't have implicit null/undefined for unspecified parameters, and afaik the map's in these languages do not provide additoinal parameters.


I like similar example: ["192", "168", "0", "1"].map(parseInt) which yields [192, NaN, 0, 1]. Makes a great question in the interview.


Personally if this was asked during an interview that didn’t allow looking up references, it would be a signal that the company is not a good place to work. It indicates the company places value on memorizing lots of programming trivia instead of valuing thoughtful research and exploring various solutions. Candidates may feel they are signing up for a brainiac contest.


It's the question to observe how candidate approaches this problem. It's not about receiving right answer.


Having written a book about JavaScript, I know the correct answer to this question.

Having also written a book about things to look for and abhor in an interview, I consider this question a strong red flag.


Is this problem avoided in Typescript? I assume it still wouldn't be if the function used with map happened to take the number & types of arguments that matched how map was declared, but that's rather less likely. Actually I'm now curious if Typescript would allow using parseInt as a parameter to a version of 'map' that was declared to only accept functions with a single argument - I'd guess not.


What would be a better spec for map() then?

I think it would be to say that [...].mapBETTER(argFunk) calls its arg-function with just a single argument. The rest of the useful ("optional") arguments should be passed by binding "this" to the array of them.

If bind() fails because the function was already bound, should throw an error.

But this brings me to my pet-peeve: Why can't I map over Objects (other than Arrays)?


It doesn't have anything to do with the specs of map().

It's part of the core semantics of the language:

  function f() {
  }
is equivalent to

  function f(a, b ,c) {
  }
because under the hood the latter is simply

  function f() {
    let a = arguments[0]
    let b = arguments[1]
    let c = arguments[2]
  }
Also no one stops you from simply extending the Array.prototype to your heart's content ¯\_(ツ)_/¯


It has to do with the fact that map() passes 3 arguments to its argument function, right?


Sure, if you want to look at it from that direction. Other languages, however, use a different default semantic for parameter passing and simply wouldn't allow using a function/lambda/delegate that doesn't explicitly take 3 arguments as well.

JS interfaces are designed with the flexibility and semantics of the language in mind (no surprise there, really) and thus try to cover most use cases. If you don't need the index or the reference to the array in your function, ignore them (e.g. by using map(function(v) { ... })). Passing a function that actually supports more than one argument is by all accounts a layer 8 error and not a fault in the spec of map() IMO.

You could just as well argue that it's bad for parseInt() to accept one or two arguments instead of explicitly requiring both. You'd have to try and convince me why one is perfectly fine while the other is not. You can of course argue that both specs are crap, that'd at least be consistent :)


Because parseInt takes multiple parameters, and map sends multiple parameters that don't match to appropriate parsing options, returning NaN.


I get Uncaught SyntaxError: Invalid or unexpected token

I'm guessing those quotes were supposed to be " rather than “ and ”.


Apple "smart quotes" strikes again!


The reason is surprising but perfectly valid once you remember the spec of map() and parseInt(). Love JS WTFs.


Yep. I actually recall someone on the team running into a similar issue during the workday.


I wouldn't even call it a JS WTF. If you use a tool wrong, why be surprised about wrong results?


Its a footgun that results from JS’s loose typing of functions, allowing them both to be called with discarded arguments and to be flexible in the number of arguments they accept.

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.


> Its a footgun that results from JS’s loose typing of functions, allowing them both to be called with discarded arguments and to be flexible in the number of arguments they accept.

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.


TypeScript has the same problem.


It helps avoiding number of arguments problems, though. Of course it doesn't change the core semantics of JS.


> It helps avoiding number of arguments problems, though

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.


he he I tried to fix this in Typescript but look like they do not care So I made and use (for my projects) my own version of typescript ;-) https://www.npmjs.com/package/@topce/typescript/v/5.1.6 There indeed you would have compile time error

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));


This is great - I will definitely check this out.

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.


Not for a long just few days. It could be too strict , probably that why they rejected PR , but it depends of callback definition. I proposed it as a flag because it is breaking change by default turn it off. But as they rejected PR in my fork I remove flag and in latest version is always on.No flag. I change one my repo to use it and need to patch some lib definition of callback to be more strict also build with typescript "skipLibCheck": true,

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 ;-)


Thanks for the details!


No problem so basically you can fix errors : add parameters with that are not used for example _index, _array or override callback type definition wrap it in JavascriptCallback


If everybody uses a tool wrong, the problem isn't with everybody, it's with the tool.


It's unexpected.

No other language does this.


> No other language does this.

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.


To be correct, all you have to do...all you have to do...is post a single example of the equivalent to the JS code that is producing the same result.

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.


Oh, I definitely could post an equivalent example in Python and C. There's simply not enough space on the sidebar to do it ;)

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.


Python does not do this:

    > 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'


Now replace

  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.


JS discarding extra positional arguments (and that being leveraged in map and similar methods) seems to be the unique bit. I don’t think any other popular language does that.


Overloading on arity isn’t that uncommon in popular languages like Java


The combination with the design of common-across-languages iterator methods like map, filter, reduce is uncommon. (Neither is doing it implicitly for all functions.)


It's not unique at all: C has this feature as well - just look at printf() and other functions that accept variable argument lists.


That's a variadic function and those have to be explicitly marked as variadic. You cannot pass in an arbitrary number of arguments to other functions.

https://en.cppreference.com/w/c/variadic


> You cannot pass in an arbitrary number of arguments to other functions.

More specifically, you could in C89 but it was removed because it's a bad feature.

https://godbolt.org/z/j4v4GT3Wd


In JS every function is a variadic function, though. The problem is that many people ignore that fact for whatever reason.

  function fn() { }
is semantically the exact same as

  function fn(a, b, c) { }
in JS.


You're discussing this as if the critical "gotcha" was with `parseInt`. It's not the critical gotcha here. The critical one is actually with the semantics of `map` which behaves differently in JS compared to just about every other language with a map (or equivalent) which passes only the elements of the sequence (or sequences in some cases) to the function. See Python, Ruby, C++ (transform is the nearest direct match), Rust, Common Lisp, Scheme, etc. Using the example with `parseInt` just demonstrates this strange, and unique, design decision.


You're looking at it myopically as well.

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.


How on Earth is that a WTF? Ignoring the specs will lead to unexpected results regardless of the language or API used.


Article is from 2011


So? The behavior it concerns still exists, and given the desire to avoid breaking backward conpatibility in JS, likely will when the article gets reposted in 2111, too.


HN is a news site. The article posted is not news.

If you post old news, it's customary to say so.

Eg, from dang, the moderator:

https://news.ycombinator.com/item?id=36398290

https://news.ycombinator.com/item?id=36366875

Etc

https://hn.algolia.com/?dateRange=all&page=0&prefix=true&que...


With talk about "cross-SoC bugs" on the frontpage, I had to think of this as an example of a "cross-API" bug.


Personally I don't see this as confusing/problematic. You just need to know .map passes multiple args (which can be extremely useful in other circumstances).

Easy fix too: ["1", "2", "3"].map(i => parseInt(i))


What's confusing/surprising is that it is different than map in many (most?) other languages with a map implementation. Like in Python it would be:

  map(some_fun, iterable, *iterables)
`some_fun` would have arity 1 or 1 + len(iterables) and be called only with the values of the passed in iterables, not the enumerated indexes or the original iterables or whatever. To get an equivalent, you'd add in an iterable that corresponded to the indexes explicitly. Same thing with Rust with its `enumerate` which would produce a sequence of pairs `(index, original_item)`. And those aren't atypical when examining `map` in other languages.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: