Object.{get, set}

Sorry if it is a duplicate but I couldn't find similar proposal

I think it would be useful to have the methods Object.get and Object.set which will get a path to the property

Examples:

const obj = {
  a: 1,
  b: 2,
  c: {
    d: {
      e: 54
    }
  }
}

console.log(Object.get(obj, 'a')); // 1
console.log(Object.get(obj, 'a.b')); // undefined
console.log(Object.get(obj, 'c.d.e')); // 54
console.log(Object.get(obj, 'c.d.e.f.g.h')); // undefined

// ---

Object.set(obj, 'a', 78);
console.log(Object.get(obj, 'a')); // 78

Object.set(obj, 'a.q.z', 87);
console.log(Object.get(obj, 'a')); // 78
console.log(Object.get(obj, 'a.q')); // undefined
console.log(Object.get(obj, 'a.q.z')); // undefined

Object.set(obj, 'c.d.e', 123);
console.log(Object.get(obj, 'c.d.e')); // 123

Object.set(obj, 'c.d.e.f.g.h', [34, 67]);
console.log(Object.get(obj, 'c.d.e.f.g.h')); // [34, 67]

The main idea based on lodash methods _.get and _.set. The differences are

  1. Lodash get method accepts default value but my proposal doesn't
  2. Both lodash get and set methods accept an array as path instead of string but my proposal doesn't

It is the basic idea. If it is worth I would be happy to create a well documented proposal on GitHub

Using strings for object navigation sounds like it would not be an improvement - there's a reason those lodash packages are deprecated in favor of optional chaining.

2 Likes

The optional chaining is one of the core functionality of my proposal. Instead of "optional chaining hell" we can just specify the path we want and get the value if the path exists and undefined otherwise

At least in stackoverflow working with objects by path are popular questions:

And a lot of people trying different solutions which can has bugs and/or performance issues but I believe that native implementation would be the best

Also what do you mean when you say deprecated? Because in the source code they aren't labeled as deprecated:

What is "optional chaining hell"

Also, those stack overflow questions were all opened over a decade ago, before optional chaining was standardized and widely available. Is there a way to see if these questions are still currently popular?

1 Like

When I write something like this

obj?.a?.b?.c?.d?.e or even worse
obj?.["first-prop"]?.["second-prop"]?.["third-prop"]?.["fourth-props"]

It is the "optional chaining hell" in my mind.

The above examples can be rewritten like this:

Object.get(obj, "a.b.c.d.e")
Object.get(obj, "first-prop.second-prop.thirds.fourth-prop")

Isn't it prettier?

About popularity of questions the get method by string is still popular on stackoverflow (at the moment of posting this answer the second link from my previous answer on 61th place)

That’s subjective, and i find it far less readable, especially compared to the first ones when using syntax highlighting.

What's ugly about it? In some languages, property access is two characters (->) instead of one (.). In most cases, JavaScript's ?. Isn't any more verbose than that. Adding Object.get() ceremony around property access seems uglier to me.

Regardless, there's a more practical reason to prefer ?. over a get() function as well. By adding ?. between each part, you're explicitly acknowledging that each of those parts may be nullish. By using a get() function, you're saying that some of the stuff in the chain may be undefined or null, but you aren't telling the code reader what. The ?. gives the code reader more information about the nature of the code, and helps future maintainers be more confident when refactoring it. I am assuming we're dealing with non-typescript code (since TypeScript currently can't provide a proper type definition to this kind of function), so it's extra important to leave hints to future code readers about the expected types of values.

1 Like

Of course it is subjective but as you can see I'm not alone

I didn't get the point about syntax highlighting. What is the problem?

When I use a lot of optional chaining in a row you still can't say to me in which part it becomes null or undefined so I can't understand the profit of typing this operator again and again

Talking about Typescript it is not a problem at all. Unfortunately I'm using phone right now and can't use the computer. But give me please a time I will post a Typescript implementation of this with type checkings

When I use a lot of optional chaining in a row you still can't say to me in which part it becomes null or undefined so I can't understand the profit of typing this operator again and again

I'll try to spell out the problem using more concrete scenarios.

Say we have a codebase with a getPort() function already in it. This getPort() function will return the config -> server -> http -> port property, or it will return 80 if something in that chain was nullish.

Our goal is to introduce a new function, getHost(), that 1. throws an error if config is nullish, and 2. returns the value of config -> server -> host (host being a newly added required property of server). This isn't a TypeScript codebase, so we have to read surrounding code to get an understanding of what types the data conforms to. In a healthy non-TypeScript codebase, there should be plenty of hints lift around so we don't have to hunt too long, as such, the task is to build this getHost() function using only getPort() as a reference. We also want to figure out if the getHost() function might return a nullish that the caller has to deal with.

Hopefully the objectives will make more sense as we get into it.

Scenario 1

We walk into the codebase and find getPort() defined as follows:

function getPort() {
  return config?.server?.http?.port ?? 80;
}

How should we define getHost()? Easy peasy, like this:

/** This function may return a nullish value */
function getHost() {
  if (config == null) throw new Error('config cannot be nullish');
  return config.server?.host;
}

Because the author of getPort() explicitly placed a ?. between each segment, we knew that the author expected that it's possible for any one of those segments to be a nullish value, therefor we too should expect the same. In getHost(), we know config won't be nullish (as we throw if it is), but config.server still might be, so we need to use ?. and warn callers that a nullish value could be returned.

Scenario 2

In a parallel universe, we walk into a codebase and find getPort() defined as follows (note that this is subtly different from before, in one spot we're using . instead of ?.).

function getPort() {
  return config?.server.http?.port ?? 80;
}

How should we define getHost() this time? The code author explicitly told us that when config is not nullish, config.server isn't either, and we can use that information to build a getHost() function that does not use ?., and that we know doesn't return a nullish value.

function getHost() {
  if (config == null) throw new Error('config cannot be nullish');
  return config.server.host;
}

Scenario 3

In another universe, Object.get() was standardized, and the codebase we walked into had a getPort() function like this:

function getPort() {
  return Object.get(config, 'server.http.port') ?? 80;
}

Now we have a problem. Did the author use Object.get() because most of the segments in this config -> server -> http -> port could be nullish (like in scenario 2) or because all of them could be nullish (like in scenario 1)? There's no way to tell. The correct implementation for getHost() will likely either be what we see in scenario 1 or 2, but you can't tell which one to use just by looking at this getPort()'s implementation - you'll have to dig deeper into the codebase.


That's the sort of problem I'm describing. It's an extremely specific example, but it's part of a more general principle - the idea that, especially in non-TypeScript codebases, it's important to leave hints everywhere you reasonably can about the types of your data, so that future you/others maintaining the code don't have to go on deep hunts to find type information.

2 Likes

Here is the simple Typescript implementation (playground):

type ObjectKeys<T> = {
    [K in keyof T & string]: T[K] extends object ? K | `${K}.${ObjectKeys<T[K]>}` : K
}[keyof T & string];

type ObjectPathValue<O, P extends ObjectKeys<O> = ObjectKeys<O>> = P extends `${infer K}.${infer R}` ? 
    (
        K extends keyof O ?
            (
                R extends ObjectKeys<O[K]> ? ObjectPathValue<O[K], R> : never
            )
        : never
    ) : (
        P extends keyof O ? O[P] : never
    );

interface Object {
    get: <O, P extends ObjectKeys<O> = ObjectKeys<O>>(obj: O, path: P) => ObjectPathValue<O, P>
}

You have very good reasoning. I didn't thought about my proposal in that way

I'm agree that JS by itself should be able to describe what developer want without any Typescript or type hints

But how many cases are there? Why can't we use your suggestion if developer really want to say that some parts are strict?

For example this:

config?.server.http?.port ?? 80

can be written like this:

Object.get(config?.server, 'http.port') ?? 80

I see the lodash method get or custom implementation in almost all projects which I work on and as you can see this possibility is very popular. I'm agree with your point 100% but is it a big problem? Because nobody forces using my proposed syntax instead of optional chaining. If developer want to be more specific he can always write it as he wish

For example we have a Proxies which allow to redefine how will get, set work. The Immer library which is very popular using Proxies almost everywhere so a lot of logic is hidden and we don't know how does it handle some cases it also hard to debug if you want to see all flow. But is it a big problem? I don't think so, it is also a part of Redux/Toolkit which is also very popular

I also can redefine standard XXX.prototype.method and nobody will know it until some bug(s) will appear. I know that it is not a best practice

I just want to say that if developer want to write something hidden or unexpected he will be always be able to do that and visa versa if he want to be more specific nothing will prohibit him from doing so

Why would you pass a dot-delimited single string into such a function? It should be

Object.get(obj, ["a", "b", "c", "d", "e"])
Object.get(obj, ["first-prop", "second-prop", "thirds", "fourth-prop"])

which is much easier to declare a type for, and which will work with property names containing dots.

You are absolutely right!

My first idea was to not be exactly like lodash but I understood later that there are cases when I can't simply use string

BTW I know from stackoverflow you helped me a lot :) What do you think about the proposal itself?

I have to admit, that TypeScript definition is pretty good. It'll choke on circular references and a couple of other edge cases, but it's impressive.

Anyways, for this point:

Because nobody forces using my proposed syntax instead of optional chaining. If developer want to be more specific he can always write it as he wish

It's a common argument in these scenarios - "you don't have to use it, so why is it a problem if it exists"? You're right that I don't have to use it, but I still have to read code that other people write using it, and if I believe the code is always going to be less readable whenever it gets used, it'll make sense that I would prefer that it simply did not exist as an option.

Anyways, I do want to turn my attention back to this for a bit:

config?.server?.http?.port ?? 80;
// vs
Object.get(config, 'server.http.port') ?? 80;
// or
Object.get(config, ['server', 'http', 'port']) ?? 80;

The main premise is that you find the ?. version ugly and the Object.get() version readable. I know this is very subjective, but I struggle to see why the latter two look nicer. Do you think you could ellaborate?

I'm looking a little more into the StackOverflow questions, as I was surprised to see a question like that still being one of the top questions.

Turns out, StackOverflow's "Frequent" sort option isn't what it seems. From their meta, it sounds like it's really related to how many questions link to it (and from what I gather, it's for "all time"). Apparently, over the course of its 14 years of existance, a lot of people have asked similar questions and linked to it.

Is it stil a popular question? Well, hard to tell, but we can see its vote summaries over time. Last year it received 15 votes, in 2022 it received 63, in 2019 it received 78, in 2016 it received 80, and in 2013 it has 9 votes. I know StackOverflow has also been receiving less traffic in general recently which could contribute to some of these statistics, but the trend does seem to be moving away from this pattern.

To clarify, I'm not necessarily using the trend I'm seeing here as an argument that this shouldn't be standardized.

We already have a huge amount of code files which uses other libraries get method

I just meant that JavaScript by it nature has a lot of ways to write the code less readable. You can change the prototype of standard objects, you can create Proxies, you can use var declaration with same names, you can change valueOf and/or toString you can use functions both like constructors and classic functions and also JS code depends on who is the host and which capabilities it provides and so on. So it is very hard to understand the medium/big projects quickly, you have to spend a lot of time to understand them well. And I guarantee that the previous list of abilities of JavaScript are much more confusing and hard to understand than just my proposal

To be honest because of it is not completed proposal yet I didn't think about it too much. I also had an idea to use something like config.get('server.http.port') or config.get(['server', 'http', 'port']) but then I thought that it can be confusing with Proxy handler get method. So I chose to use static method of Object

The static method also allows converting libraries get method into Object.get more easily.

It is very hard to me to argue which one is prettier but at least in these examples like this:

obj?.["first-prop"]?.["second-prop"]?.["third-prop"]?.["fourth-props"]

if we rewrite in the proposal way it becomes to:

Object.get(obj, "first-prop.second-prop.thirds.fourth-prop")

So I read it like this "Find this path in given object and return the value if the path exists, otherwise return undefined"

Also if we recall that I also suggest creating set method it becomes more easy and useful for me. Let's say that you request settings of one of your client from API and it returns the JSON. You want to set a default value in deeply nested setting. Without this proposal you will write something like this:

const settings = await fetch("some-url");

settings.feed ??= {};
settings.feed.filters ??= {}
settings.feed.filters.priceRange ??= {};
settings.feed.filters.priceRange.min ??= 0;
settings.feed.filters.priceRange.max ??= 100;

but with the proposal I can rewrite it like this:

const settings = await fetch("some-url");

const priceRangePath = "feed.filters.priceRange";
const priceMaxPath = `${priceRangePath}.max`;
const priceMinPath = `${priceRangePath}.min`;

const priceMin = Object.get(settings, priceMinPath) ?? 0;
const priceMax = Object.get(settings, priceMaxPath) ?? 100;

Object.set(settings, priceMinPath, priceMin);
Object.set(settings, priceMaxPath, priceMax);

// Or like one-liner

Object.set(settings, priceMinPath, Object.get(settings, priceMinPath) ?? 0);
Object.set(settings, priceMaxPath, Object.get(settings, priceMaxPath) ?? 100);

And now if one day the filters will move from feed into user-preferences I will just rewrite the priceRangePath. Also as you can see I can easily create the abstraction on the paths that I need and change them only in one place. I understand that I can also create abstarctions with functions for previous method but for me string constants are much less likely to cause bugs

This way is much cleaner and easier to read for me than the first approach. But again, it's subjective. For me (and not only me) it's easier to say "get value from path" and "set value by path" than to control each part of path manually in detail

1 Like

StackOverflow mostly used by anonymous users who just search the question and a lot of StackOverflow members doesn't give a vote to the question itself. So I would like to say that if it is true

and the question only by itself was Viewed 387k times then for me it is something popular.

I can assume the reason people stop searching the questions like this because there are already a several giant utility libraries that provide similar capabilities

I created a proposal. If anyone like the idea, please give a star and/or share the proposal with others

Deep accessing and setting are common enough needs from my perspective, although the Committee would demand clear evidence of demand if they are presented. The Stack Overflow question is a start but probably not sufficient. We would need to dig into how often the Lodash functions etc. are used.

For what it’s worth, I do like the list-of-keys form of your proposed Object.get and Object.set, like Object.get(x, ["a", "b"]). They remind me of Clojure’s get-in and set-in functions.

I am not so big of a fan of the period-delimited string form, which would probably cause quite a few memory allocations in hot paths. (The array containing the list of keys, at least, might be easier to optimize away.)

I sadly don’t have the bandwidth to champion this for the foreseeable future—there’s a long backlog. But I think it may be worth eventually presenting for Stage 1…if we can get better evidence of developer demand, e.g., statistics of _.get usage.

(I agree that this feature should not use new syntax. The engine implementors would be very unlikely to support maintaining syntax for this feature.)

2 Likes