Skip to content

Commit 3902c64

Browse files
authoredFeb 17, 2022
Add deepKeys() (#94)
1 parent 2c1bbfb commit 3902c64

File tree

5 files changed

+132
-2
lines changed

5 files changed

+132
-2
lines changed
 

‎index.d.ts

+27
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,30 @@ console.log(getProperty(object, escapedPath));
131131
```
132132
*/
133133
export function escapePath(path: string): string;
134+
135+
/**
136+
Returns an array of every path. Plain objects are deeply recursed and are not themselves included.
137+
138+
This can be useful to help flatten an object for an API that only accepts key-value pairs or for a tagged template literal.
139+
140+
@param object - The object to iterate through.
141+
142+
@example
143+
```
144+
import {getProperty, deepKeys} from 'dot-prop';
145+
146+
const user = {
147+
name: {
148+
first: 'Richie',
149+
last: 'Bendall',
150+
},
151+
};
152+
153+
for (const property of deepKeys(user)) {
154+
console.log(`${property}: ${getProperty(user, property)}`);
155+
//=> name.first: Richie
156+
//=> name.last: Bendall
157+
}
158+
```
159+
*/
160+
export function deepKeys(object: unknown): string[];

‎index.js

+42
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,45 @@ export function escapePath(path) {
284284

285285
return path.replace(/[\\.[]/g, '\\$&');
286286
}
287+
288+
// The keys returned by Object.entries() for arrays are strings
289+
function entries(value) {
290+
if (Array.isArray(value)) {
291+
return value.map((value, index) => [index, value]);
292+
}
293+
294+
return Object.entries(value);
295+
}
296+
297+
function stringifyPath(pathSegments) {
298+
let result = '';
299+
300+
for (let [index, segment] of entries(pathSegments)) {
301+
if (typeof segment === 'number') {
302+
result += `[${segment}]`;
303+
} else {
304+
segment = escapePath(segment);
305+
result += index === 0 ? segment : `.${segment}`;
306+
}
307+
}
308+
309+
return result;
310+
}
311+
312+
function * deepKeysIterator(object, currentPath = []) {
313+
if (!isObject(object)) {
314+
if (currentPath.length > 0) {
315+
yield stringifyPath(currentPath);
316+
}
317+
318+
return;
319+
}
320+
321+
for (const [key, value] of entries(object)) {
322+
yield * deepKeysIterator(value, [...currentPath, key]);
323+
}
324+
}
325+
326+
export function deepKeys(object) {
327+
return [...deepKeysIterator(object)];
328+
}

‎index.test-d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {expectTypeOf} from 'expect-type';
2-
import {getProperty, setProperty, hasProperty, deleteProperty} from './index.js';
2+
import {getProperty, setProperty, hasProperty, deleteProperty, deepKeys} from './index.js';
33

44
expectTypeOf(getProperty({foo: {bar: 'unicorn'}}, 'foo.bar')).toBeString();
55
expectTypeOf(getProperty({foo: {bar: 'a'}}, 'foo.notDefined.deep')).toBeUndefined();
@@ -17,3 +17,5 @@ expectTypeOf(setProperty(object, 'foo.bar', 'b')).toEqualTypeOf(object);
1717
expectTypeOf(hasProperty({foo: {bar: 'unicorn'}}, 'foo.bar')).toEqualTypeOf<boolean>();
1818

1919
expectTypeOf(deleteProperty({foo: {bar: 'a'}}, 'foo.bar')).toEqualTypeOf<boolean>();
20+
21+
expectTypeOf(deepKeys({foo: {bar: 'a'}})).toEqualTypeOf<string[]>();

‎readme.md

+23
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,29 @@ console.log(getProperty(object, escapedPath));
108108
//=> '🍄 The princess is in another castle!'
109109
```
110110

111+
### deepKeys(object)
112+
113+
Returns an array of every path. Plain objects are deeply recursed and are not themselves included.
114+
115+
This can be useful to help flatten an object for an API that only accepts key-value pairs or for a tagged template literal.
116+
117+
```js
118+
import {getProperty, deepKeys} from 'dot-prop';
119+
120+
const user = {
121+
name: {
122+
first: 'Richie',
123+
last: 'Bendall',
124+
},
125+
};
126+
127+
for (const property of deepKeys(user)) {
128+
console.log(`${property}: ${getProperty(user, property)}`);
129+
//=> name.first: Richie
130+
//=> name.last: Bendall
131+
}
132+
```
133+
111134
#### object
112135

113136
Type: `object | array`

‎test.js

+37-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import test from 'ava';
2-
import {getProperty, setProperty, hasProperty, deleteProperty, escapePath} from './index.js';
2+
import {getProperty, setProperty, hasProperty, deleteProperty, escapePath, deepKeys} from './index.js';
33

44
test('getProperty', t => {
55
const fixture1 = {foo: {bar: 1}};
@@ -407,6 +407,42 @@ test('escapePath', t => {
407407
});
408408
});
409409

410+
test('deepKeys', t => {
411+
const object = {
412+
'a.b': {
413+
c: {
414+
d: [1, 2, 3],
415+
e: '🦄',
416+
f: 0,
417+
},
418+
'': {
419+
a: 0,
420+
},
421+
},
422+
'': {
423+
a: 0,
424+
},
425+
};
426+
const keys = deepKeys(object);
427+
428+
t.deepEqual(keys, [
429+
'a\\.b.c.d[0]',
430+
'a\\.b.c.d[1]',
431+
'a\\.b.c.d[2]',
432+
'a\\.b.c.e',
433+
'a\\.b.c.f',
434+
'a\\.b..a',
435+
'.a',
436+
]);
437+
438+
for (const key of keys) {
439+
t.true(hasProperty(object, key));
440+
}
441+
442+
t.deepEqual(deepKeys([]), []);
443+
t.deepEqual(deepKeys(0), []);
444+
});
445+
410446
test('prevent setting/getting `__proto__`', t => {
411447
setProperty({}, '__proto__.unicorn', '🦄');
412448
t.not({}.unicorn, '🦄'); // eslint-disable-line no-use-extend-native/no-use-extend-native

0 commit comments

Comments
 (0)
Please sign in to comment.