Skip to content

Instantly share code, notes, and snippets.

@olayemii
Last active April 15, 2020 20:23
Show Gist options
  • Save olayemii/948e5f611ee3f83e811d0040a2f0c4d4 to your computer and use it in GitHub Desktop.
Save olayemii/948e5f611ee3f83e811d0040a2f0c4d4 to your computer and use it in GitHub Desktop.
A simple helper function for getting deeply nested object property
const getObjectProperty = (obj, path, defaultValue="", returnUndefined=true) => {
const checkForDefaultValue = value =>
value !== undefined ? value : undefined;
if (path === undefined) {
return obj;
}
try {
const value = path.split('.').reduce((o, i) => o[i], obj);
if (value === undefined && returnUndefined) return value;
return value !== undefined ? value : checkForDefaultValue(defaultValue);
} catch (e) {
if (e instanceof TypeError) return checkForDefaultValue(defaultValue);
throw e;
}
};
/*
const a = {
b: {
c: [
{
d: {
e: 14
}
},
[16, 11]
],
d: 12
}
};
getObjectProperty(a, 'b.d', "Default!");
// 12
getObjectProperty(a, 'b.e', "Not a property");
// undefined
getObjectProperty(a, 'b.e', "Not a property", false);
// Not a property
getObjectProperty(a, 'b.c.0.d.e', "Not a property!");
// 14
getObjectProperty(a, 'b.c.1.0', "Not a property!");
//16
*/
@ahkohd
Copy link

ahkohd commented Apr 3, 2020

Nice one!

@Just4Ease
Copy link

What if the path we need is inside an array of one of the fields in the object?
So, I'll suggest, let's refactor to call itself based on the type of field that has children. 😄

So, if the type of the field is an object, it calls itself,
if the type of field is an array, it traverses the tree or plucks from the tree and then return that found field.
if the type of field is an array that holds objects, it calls itself to operate at this level.

Thus a recursive function call to only breaks out when it found the required, else the default value or undefined is thrown.

This helps people in .ts to stop doing a?.b?.c?.d? === something 😄
Well done brother 👍

@ahkohd
Copy link

ahkohd commented Apr 6, 2020

Let's clarify usage:

const a = {b: c: [{d: {e: 14}}], [16, 11]]], d: 12};

getObjectProperty(a, 'b.d',  "Not a property!");
// 12

getObjectProperty(a, 'b.e', "Not a property!");
// Not a property!

getObjectProperty(a, 'b.c.0.d.e', "Not a property!");
// 14

getObjectProperty(a, 'b.c.1.0', "Not a property!");
// 16

@olayemii
Copy link
Author

olayemii commented Apr 6, 2020

Thanks @Just4Ease , @ahkohd :-)

@ahkohd
Copy link

ahkohd commented Apr 13, 2020

@olayemii
I wrote some tests for the method, but the last test did not pass until I have to modify your code to check if it's trying to return undefined.

UPDATE SNIPPET:

const getObjectProperty = (obj, path, defaultValue) => {
  const checkForDefaultValue = value =>
    value !== undefined ? value : undefined;

  if (path === undefined) {
    return obj;
  }
  try {
    const value = path.split('.').reduce((o, i) => o[i], obj);
    return value !== undefined ? value : checkForDefaultValue(defaultValue);
  } catch (e) {
    if (e instanceof TypeError) return checkForDefaultValue(defaultValue);
    throw e;
  }
};

export default getObjectProperty;

@ahkohd
Copy link

ahkohd commented Apr 13, 2020

Oh yes, the tests I wrote:

describe('getObjectProperty', () => {
  const testObject = { a: 'A', b: 'B', c: [1, 2] };

  it('should check if property `a` exists', () => {
    const test = getObjectProperty(testObject, 'a');
    expect(test).toBe('A');
  });

  it('should check if property `d` exists', () => {
    const test = getObjectProperty(testObject, 'd');
    expect(test).toBeUndefined();
  });

  it('should check if property `c.0` exists', () => {
    const test = getObjectProperty(testObject, 'c.0');
    expect(typeof test).toBe('number');
    expect(test).toBe(1);
  });

  it('should check if property `c` does not exist.', () => {
    const test = getObjectProperty(testObject, 'c', 'Not Found');
    expect(Array.isArray(test)).toBe(true);
    expect(test).not.toBe('Not Found');
  });

  it(`should check if property 'd' exists, returns default value if it doesn't`, () => {
    const test = getObjectProperty(testObject, 'd', 'Not Found');
    expect(typeof test).toBe('string');
    expect(test).toBe('Not Found');
  });
});

@olayemii
Copy link
Author

Oh yes, the tests I wrote:

describe('getObjectProperty', () => {
  const testObject = { a: 'A', b: 'B', c: [1, 2] };

  it('should check if property `a` exists', () => {
    const test = getObjectProperty(testObject, 'a');
    expect(test).toBe('A');
  });

  it('should check if property `d` exists', () => {
    const test = getObjectProperty(testObject, 'd');
    expect(test).toBeUndefined();
  });

  it('should check if property `c.0` exists', () => {
    const test = getObjectProperty(testObject, 'c.0');
    expect(typeof test).toBe('number');
    expect(test).toBe(1);
  });

  it('should check if property `c` does not exist.', () => {
    const test = getObjectProperty(testObject, 'c', 'Not Found');
    expect(Array.isArray(test)).toBe(true);
    expect(test).not.toBe('Not Found');
  });

  it(`should check if property 'd' exists, returns default value if it doesn't`, () => {
    const test = getObjectProperty(testObject, 'd', 'Not Found');
    expect(typeof test).toBe('string');
    expect(test).toBe('Not Found');
  });
});

Hmm, @ahkohd I see, but in this case the value of testObject.d is actually undefined 🤔 Should the default value really suffice when it's not trying to read a property from undefined?

@ahkohd
Copy link

ahkohd commented Apr 14, 2020

@olayemii
The issue is that this line of code can result to undefined and since you used return, undefined will be returned.

return path.split(".").reduce((o, i) => o[i], obj);

Try this in your browser's console with your original function:

const testObject = { a: 'A', b: 'B', c: [1, 2] };
getObjectProperty(testObject, 'd', 'Not Found');
// results in undefined instead of 'No Found'

@olayemii
Copy link
Author

@ahkohd

getObjectProperty(testObject, 'd', 'Not Found');

I feel this should actually return undefined, because testObject.d is actually having a value of undefined

My initial idea was that this

return path.split(".").reduce((o, i) => o[i], obj);

returns an undefined except when we are trying to read from undefined like undefined.name that is when the catch block gets invoked and a defaultValue can be used.

@ahkohd
Copy link

ahkohd commented Apr 14, 2020

Very well, for my use case the later function works. Maybe you should provide a flag.

@olayemii
Copy link
Author

Very well, for my use case the later function works. Maybe you should provide a flag.

@ahkohd okay, I added a flag to allow undefined returns or fall back to a set default value, I also included your example. 👌🏾

@ahkohd
Copy link

ahkohd commented Apr 15, 2020

Great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment