Skip to content

Commit

Permalink
feat(utilities): add getDeepObjectKeys (#413)
Browse files Browse the repository at this point in the history
* feat(utilities): add getDeepObjectKeys

* chore: here be dragons

permalink: http://whatthecommit.com/29d3421ded23e53d3882f6fb6aa758e3

* chore: continued development...

permalink: http://whatthecommit.com/d5a41f912fb8d73730cc52801ca6ab92
  • Loading branch information
favna committed Aug 4, 2022
1 parent 868bd9b commit b8b0883
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/utilities/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './lib/filterNullAndUndefinedAndEmpty';
export { filterNullAndUndefinedAndEmpty as filterNullishOrEmpty } from './lib/filterNullAndUndefinedAndEmpty';
export * from './lib/filterNullAndUndefinedAndZero';
export { filterNullAndUndefinedAndZero as filterNullishOrZero } from './lib/filterNullAndUndefinedAndZero';
export * from './lib/getDeepObjectKeys';
export * from './lib/hasAtLeastOneKeyInMap';
export * from './lib/inlineCodeBlock';
export * from './lib/isClass';
Expand Down
56 changes: 56 additions & 0 deletions packages/utilities/src/lib/getDeepObjectKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { isObject } from './isObject';
import type { AnyObject } from './utilityTypes';

/**
* Flattens an object to a list of its keys, traversing deeply into nested objects and arrays of objects.
*
* @note By default Nested array values are flattened to `arrayKey.${index}.subKey`. This can be changed to `arrayKey[${index}].subKey` by setting `arrayKeysBracedIndex` to `true`.
*
* @param obj The object of which to deeply retrieve its keys
* @returns An array of strings holding the keys of the object
*/
export function getDeepObjectKeys<T>(
obj: AnyObject<T>,
{ arrayKeysIndexStyle = 'dotted' }: GetDeepObjectKeysOptions = { arrayKeysIndexStyle: 'dotted' }
): string[] {
const keys: string[] = [];

for (const [key, value] of Object.entries(obj)) {
if (Array.isArray(value)) {
for (const [index, innerValue] of value.entries()) {
const arraySubKeys = getDeepObjectKeys(innerValue);
keys.push(
...arraySubKeys.map((arraySubKey) => {
switch (arrayKeysIndexStyle) {
case 'braces-with-dot':
return `${key}[${index}].${arraySubKey}`;
case 'braces':
return `${key}[${index}]${arraySubKey}`;
case 'dotted':
default:
return `${key}.${index}.${arraySubKey}`;
}
})
);
}
} else if (isObject(value)) {
const objectSubKeys = getDeepObjectKeys(value);
keys.push(...objectSubKeys.map((subKey) => `${key}.${subKey}`));
} else {
keys.push(key);
}
}

return keys;
}

/**
* The options for {@link getDeepObjectKeys}
*/
export interface GetDeepObjectKeysOptions {
/**
* Whether to use `.${index}.` (`'dotted'`), `[${index}].`, (`'braces-with-dot'`) or `[${index}]` (`'braces'`) to separate array keys
* @default 'dotted'
*/
arrayKeysIndexStyle?: 'dotted' | 'braces-with-dot' | 'braces';
}
21 changes: 21 additions & 0 deletions packages/utilities/tests/getDeepObjectKeys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getDeepObjectKeys, GetDeepObjectKeysOptions, NonNullObject } from '../src';

describe('getDeepObjectKeys', () => {
const scenarios: Scenario[] = [
[{ a: 1, b: 2 }, {}, ['a', 'b']],
[{ a: [[]] }, {}, []],
[{ a: [[{ b: 0 }]] }, {}, ['a.0.0.b']],
[{ a: { b: 1, c: 2 }, d: 3 }, {}, ['a.b', 'a.c', 'd']],
[{ a: [{ b: 1, c: 2 }, { d: 3 }] }, {}, ['a.0.b', 'a.0.c', 'a.1.d']],
[{ a: [{ b: 1, c: 2 }, { d: 3 }] }, { arrayKeysIndexStyle: 'braces' }, ['a[0]b', 'a[0]c', 'a[1]d']],
[{ a: [{ b: 1, c: 2 }, { d: 3 }] }, { arrayKeysIndexStyle: 'braces-with-dot' }, ['a[0].b', 'a[0].c', 'a[1].d']],
[{ a: [{ b: 1, c: 2 }, { d: 3 }] }, { arrayKeysIndexStyle: 'dotted' }, ['a.0.b', 'a.0.c', 'a.1.d']],
[{ a: [{ b: 1, c: 2 }, { d: 3 }], e: 4, f: { g: { h: { i: { j: { k: 5 } } } } } }, {}, ['a.0.b', 'a.0.c', 'a.1.d', 'e', 'f.g.h.i.j.k']]
];

test.each(scenarios)('GIVEN %j with %j THEN keys should equal %j', (inputObj, options, outputStrings) => {
expect(getDeepObjectKeys(inputObj, options)).toEqual(outputStrings);
});
});

type Scenario = [NonNullObject, GetDeepObjectKeysOptions, string[]];

0 comments on commit b8b0883

Please sign in to comment.