Is it too late to fix `Object.entries` ???

There's no symmetry between Object.fromEntries and Object.entries returned values, because Object.fromEntries accepts Symbol as keys, while Object.entries silently ignore symbols.

Object.fromEntries([[Symbol('ok'), 123]]);
// {Symbol(ok): 123}

Object.entries({[Symbol('ok')]: 123});
// [] <- empty , Symbol lost

Could we add an extra boolean value to the signature so that true means, include any other kind of key too?

Object.entries(reference, true);
// or ...
Object.entries(reference, {symbols: true});

It's baffling me Symbol got introduced way before Object.entries and yet these got ignored so that we have basically no native direct API way to map entries, including symbols, from a generic object ... workarounds are possible, of course, but the asymmetric entries fromEntries shenanigan is extremely hard to reason about, imho.

2 Likes

Some rationale is in the original Object.entries proposal repo:

Consistency with Object.keys is paramount in this proposal‘s opinion. A follow-on proposal for an iterator, however, could likely be Reflect.ownValues and Reflect.ownEntries , which would complete the triplet with Reflect.ownKeys , providing an array of both string-valued and symbol-valued properties

While Symbol was added before Object.entries, Object.fromEntries was added after Object.entries. So Object.entries was following Object.keys. And Object.fromEntries is desgined to create an object from Object.entries while also providing support for symbols as an alternative input. So the relationship is in that direction: Object.fromEntries(Object.entries(o)).

With the options bag solution you suggest, would it be more consistent if that was also available on the other static methods too? Object.keys(obj, { symbols: true }) for example.

A userland implementation if needed:
function entries(obj, { symbols = false } = {}) {
  obj = Object(obj);
  return Reflect.ownKeys(obj)
    .flatMap(k => {
      if (!symbols && typeof k === 'symbol') {
        return [];
      }
      const desc = Reflect.getOwnPropertyDescriptor(obj, k);
      if (! desc?.enumerable) {
        return [];
      }
      return [[k, obj[k]]];
    });
}
1 Like

I am not sure I am following. There is no rationale about asymmetric API between entries and fromEntries, these are just not symmetric and loosely coupled, but the proposal on top of Reflect for ownX sounds great, although no browser exposes these today.

Beside the fact I don't think anyone should change Object.keys because that's really legacy, can we fix instead a badly implemented Object.entries that fails at preserving what Object.fromEntries returns? Or shall we fix Object.fromEntries because it doesn't survive what Object.entries expects?

edit I guess my main concern is how entries are even described in ECMAScript ... do these contain Symbols, or don't? If these do ... why Object.entries ignores symbols?

P.S. I believe, by specs, in no circumstances Reflect.getOwnPropertyDescriptor(obj, k) would ever return not a descriptor, within a Reflect.ownKeys loop, so that if (! desc?.enumerable) got me thinking :thinking:

Object.fromEntries was added to the spec after Object.entries. So Object.fromEntries was the one that introduced the asymmetry. It could have ignored entries with symbol keys to preserve symmetry, but that seems less useful.

Object.entries ignores symbols so that it follows the pattern set by Object.keys, and is effectively: Object.keys(o).map(k => [k, o[k]]).

All types of exotic behaviors are possible with proxy:

const p = new Proxy({}, { ownKeys() { return ["a"]; } });

Reflect.ownKeys(p); // ["a"]
Reflect.getOwnPropertyDescriptor(p, "a"); // undefined

Are you still explaining to me what happens when I've filed an issue about what happens? I am not sure how to move forward here ... you keep telling me the asymmetry is what landed is not helping .. can we do something about it? would be my question, thanks.

P.S. thanks for the proxy thing ... I guess I forgot about that case :+1: ... still, the issue here is about something completely unrelated, thanks!

Maybe we can have a conversation with me asking then why Object.fromEntries didn't follow the pattern set by Object.entries ... do you understand where my initial post comes from?

History is clear, but developers expectations are messed up :man_shrugging:

If this adds anything, Map was born as better alternative to hash-maps in JS (objects) and every modern API understands entries, as sequence of key and value pairs, where key can be anything, including, and specially, own Symbols part of the obejct that's being introspected.

Now, because Map itself accepts an array of entries, it's not clear why Object.entries should not return all object entries and instead return a filter of these, for nobody benefit ... because if there was a benefit in not having Symbols on the way, Object.fromEntries would've filtered Symboles indeed ... isn't this logic and why we like programming, after all?

So ... can we do anything or this is going to be the next shenanigan about JS to explain in books?

Thank you for anything that could help moving forward here :pray:

To answer your question, it is certainly too late to change Object.entries in this way, yes. And it is almost certainly too late change Object.fromEntries (as well as being undesirable).

We could in theory add a new API, or an additional parameter to Object.entries, which would be like Object.entries but also with symbols, but we can't change the existing thing without breaking a bunch of code.

I don't think it's necessarily a bad thing for a pair of APIs to be stricter in what they produce than what they consume. For example, Object.fromEntries will also allow you to pass in numeric keys, which it will cast to strings, whereas Object.entries will never produce numbers for keys. But that's fine.

1 Like

I personally don't consider asymmetry fine, especially for serialization purposes ... JSON.stringify(JSON.parse(JSON.stringify(object))) produces exactly the same parse object the initial stringify passed to parse, but here we have APIs losing stuff so that Object.fromEntries(Object.entries(Object.fromEntries(entries))) won't represent the initial object that entries represented at the beginning ... a broken protocol to be more precise.

Can JSON.parse ignore comments and functions? sure ... but is that a valid protocol when it comes to JSON expectations? nope

Hence my post, and my questions ... is this really going to be yet another shenanigan to explain about JS becuase "yeah, asymmetry is fine in this case half based on legacy case one" ???

I hope nope too.

As I said, it is too late to change either Object.entries or Object.fromEntries. If you regard this behavior as a problem, then yes, it's a problem you'll need to live with.

both methods are meant to represent objects ... having keys as numbers rather than strings, or even boolean, or any other primitive coercing to string just fine, is not an issue here, the issue is about loosing symbols instead in the process, which is also another primitive ... why all others can be perpetuated as key, even as string, while symbol, a primitive, cannot?

Nobody wants to change Object.fromEntries, but I am very curious to see data about the claim it's too late to change Object.entries, as we're not in ES5 to ES2015 era anymore, we're way beyond, and I believe nothing out there is using Object.entries(...args) ... please prove me wrong?

Thank you!

I believe nothing out there is using Object.entries(...args)

Oh, sorry, by "fix" I was talking about changing the behavior when called as it currently is, without a second argument. I think it probably would be web-compat to add an overload like Object.entries(arg, { symbols: true }).

1 Like

if it would now, better amend that signature sooner than later, or in years it might not be web-compat anymore, and everyone would lose a log term opportunity to perpetuate or reflect entries across serialization.

Thanks for considering this change to the current API.

Your primary argument here seems to be the asymetry, but I suspect there's something deeper going on.

If it was just asymetry, I would suspect that you would also dislike this (as others have mentioned), and would want to get this fixed as well.

but you seem to be ok with this behavior:

> Object.entries(Object.fromEntries([[4, 2]]))
[ [ '4', 2 ] ]

Also, to fix asymetry, we could technically go with another route of giving Object.entries() an optional parameter to make it throw an error if you try to pass in symbol keys. But, I suspect you don't particularly like this solution either, even though it would fix the asymetry just as much.

So, perhaps asymetry isn't the root issue, perhaps the root issue is that you simply want the ability to easily extract both the symbol keys and the normal keys from an object? And there's currently no easy way to do this? If so, we could focus on this as the problem statement, discuss use-cases for why this is important, etc.

1 Like

nah, I've explained it in here

no reason to stress too much what I never intended to bring to this issue ... but glad you agree it's a mess already.

Does your use case require ignoring non-enumerables or reifying accessor properties? If not, Object.defineProperties({}, Object.getOwnPropertyDescriptors(o)) seems like it’d work.

1 Like

I am not looking for alternative solutions here, I am suggesting to fix the bad symmetry between entries and fromEntries. I can use Reflect too and also the goal is to have a Map representing entries, not objects.

There’s no technical reason, as has been said, we couldn’t add an options bag to entries to include symbols - but for consistency, we’d need to also add it to keys and values, and it’d have to be a pretty compelling use case for the wider committee to be convinced it was worth it, given that alternatives exist.

Object.fromEntries([[1, 'a']]) produces exactly the same object as Object.fromEntries([['1', 'a']]) so I don't understand the deal about other primitives or keys ... symbol type, on the other side, is completely discarded with Object.entries so that the original object cannot be represented via entries.

There's no reason to fix the key type, there are reasons to fix losing keys completely ... these are two different issues, and I am not sure why we should talk about both issues in the same post.

Also worth saying that Reflect.ownEntries won't necessarily be better because Object.fromEntries would still make all entries enumerable and it will lose the getter/setter kind, so that is also not a solution to the fact Object.entries loses symbols in the process.

edit in short: Object.entries is the one that needs a fix ... Object.fromEntries is perfectly and practically fine as it is, as the output is the same given input as [key, value] pairs, regardless of the key primitive type. Let's fix what's needed and maybe take time in the future to fix what's not really important?