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