1

I have a use case where I need to use a string to match object values

For example

'one': {
    'two': {
            'three': 'val'
        }
    }
}

split and reduce seems to be the way to go about this, but I have not gotten it to work using the code below

const string = 'one.two.three';
const test = string
             .split('.')
             .reduce((obj, key) => {
                 return obj[key]
             });
console.log(test); 
//expected one['two']['three'] 

[ Edit to add more context ] I have an HTML Template (angular 7) that calls the sort function

(click)="sortColumn('spn.subscriptionId')"

I have the following object to keep track of sort order for each column

public sortableColumns = [
    { colName: 'name', colSortDir: 'none'},
    { colName: 'spn.subId', colSortDir: 'none'},
    { colName: 'regionName', colSortDir: 'none'},
    { colName: 'customerName', colSortDir: 'none'}
];

and the sortColumn function. this.subscriptions comes from a rest api. the sort function works if there is a rest api object only has one level - 'name' but does not work for the nested object - spn.subscriptionId

public sortColumn(column : string) {
    // resets other columns 
    this.sortableColumns.forEach((el) => {
        if (el.colName !== column) {
            el.colSortDir = 'none';
        }
    });

    // matches the column name passed with sortableColumns to set order in UI
    const col = this.sortableColumns.filter((el) => {
        return el.colName === column;
    });

    // sorting of the array object. this.subscriptions comes fr
    if (col[0].colSortDir === 'asc') {
        col[0].colSortDir = 'dsc';
        this.subscriptions.sort((val1, val2) => {
            if (val1[column] < val2[column]) {
                return -1;
            } else if (val1[column] > val2[column]) {
                return 1;
            } else {
                return 0;
            }
        });
    } else if (col[0].colSortDir === 'none' || col[0].colSortDir === 'dsc') {
        col[0].colSortDir = 'asc';
        this.subscriptions.sort((val1, val2) => {
            if (val1[column] > val2[column]) {
                return -1;
            } else if (val1[column] < val2[column]) {
                return 1;
            } else {
                return 0;
            }
        });
    }
}
n00b3
  • 73
  • 2
  • 6
  • Does this answer your question? [Accessing nested JavaScript objects with string key](https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key) – Heretic Monkey Feb 13 '20 at 18:00
  • I don't need to access the value of the object, but rather construct the lookup key one['two']['three'] as this then gets used in a sorting function for a table – n00b3 Feb 13 '20 at 18:11
  • There is no "lookup key" though. `one['two']['three']` is essentially meaningless, unless you plan on using `eval` to get the data, in which case you might as well use your original `'one.two.three'`. Perhaps you can [edit] your question to show more of how the output is used in this sorting function, we can help you get what you need. – Heretic Monkey Feb 13 '20 at 18:16
  • thanks, I have edited my questions to add more context. – n00b3 Feb 13 '20 at 18:53
  • I'd suggest making the code a [mcve] so that someone can drop it into a standalone IDE like [The TypeScript playground](http://www.typescriptlang.org/play/) to demonstrate the issue. It looks like you're not passing the actual object you want to start with into your code. – jcalz Feb 13 '20 at 19:01

3 Answers3

1

There is no magic "lookup key" that you can plug in to the bracket notation obj[key] and have it traverse multiple nested objects.

Given your expected use case, it makes the most sense to have a lookup function that takes a compound key and uses your .split().reduce() method to fetch the desired value in the nested objects.

So, something like this:

function lookup(obj : object, key : string) : any {
    return key.split('.').reduce((o, k) => o && o[k], obj);
}

Used like this:

    this.subscriptions.sort((val1, val2) => {
        return lookup(val1,column) - lookup(val2,column);
    });
Klaycon
  • 10,599
  • 18
  • 35
0

It looks like your problem was not passing the actual object in as the initialValue parameter.

We can write a nestedProp() function which takes an object and a path string and walks through it:

function nestedProp(obj: object, path: string): unknown {
    return path.split(".").reduce((o, k) => o ? (o as any)[k] : undefined, obj);
}

All I've really done is add obj as initialValue. I also guard against o[k] being null or undefined so that the code doesn't throw runtime errors if you pass a bad string.

Also note that the type definitions I've given are really very loose; obj is any object, path is any string, and it returns unknown. That's because TypeScript can't do string manipulation at the type level, so it has no idea that "one.two.three".split(".") will result in ["one", "two", "three"] and therefore no idea that nestedProp() should produce a string instead of an undefined which is what happens when you pass in, say, "oops.not.a.property". It's possible to tighten the definitions up a little, but it doesn't seem worth it. To be safe you should test that the output is what you want.


So, let's test it. Say your object is this:

const myObj = {
    'one': {
        'two': {
            'three': 'val'
        }
    }
}

Then you can call nestedProp() like this:

const res1 = nestedProp(myObj, "one.two.three");
console.log(res1); // "val"
console.log((typeof res1 === "string" ? res1 : "oops").toUpperCase()); // VAL

const res2 = nestedProp(myObj, "three.two.one");
console.log(res2); // undefined
console.log((typeof res2 === "string" ? res2 : "oops").toUpperCase()); // OOPS

Looks good to me. Note that I'm testing if the output is a string before assuming it is one, and using "oops" in the case that it isn't. Okay, hope that helps; good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • ...the `nestedProp` function being what the proposed duplicate answered, using JavaScript, so minus the type decorations... – Heretic Monkey Feb 14 '20 at 02:17
-1
let string = "one.two.three.four.five.six";

let stringArr = string.split('.');
let obj = {
  [stringArr[stringArr.length - 1]]: "value",
}
for (let i = stringArr.length - 2; i >= 0; i--) {
  this[stringArr[i]] = obj
  obj = Object.assign({}, this)
  delete this[stringArr[i]]
}
console.log(JSON.stringify(obj))

This works for any number of nesting.

{"one":{"two":{"three":{"four":{"five":{"six":"value"}}}}}}

I hope this is what you want