Repository: sindresorhus/dot-prop (js)

Issue:
`deepKeys` helper?
Hi, I've implemented this a couple of times but wondered whether there'd be any interest in including it in this library:

```ts
const obj = { foo: { bar: 1, baz: { 'a.b': 2 } } }

deepKeys(obj) // ['foo.bar', 'foo.baz.a\\.b']
```

Worth doing here because the `\\.` is a convention invented by dot-prop (as far as I'm aware), so it makes sense for `deepKeys` to return paths that `getProperty` will later understand.

Pull request description:
Add `deepKeys()`
Fixes #93

Patch:
diff --git a/index.d.ts b/index.d.ts
index f812a4c..0eaa13a 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -131,3 +131,30 @@ console.log(getProperty(object, escapedPath));
 ```
 */
 export function escapePath(path: string): string;
+
+/**
+Returns an array of every path. Plain objects are deeply recursed and are not themselves included.
+
+This can be useful to help flatten an object for an API that only accepts key-value pairs or for a tagged template literal.
+
+@param object - The object to iterate through.
+
+@example
+```
+import {getProperty, deepKeys} from 'dot-prop';
+
+const user = {
+	name: {
+		first: 'Richie',
+		last: 'Bendall',
+	},
+};
+
+for (const property of deepKeys(user)) {
+	console.log(`${property}: ${getProperty(user, property)}`);
+	//=> name.first: Richie
+	//=> name.last: Bendall
+}
+```
+*/
+export function deepKeys(object: unknown): string[];
diff --git a/index.js b/index.js
index 11d9895..3b78ad4 100644
--- a/index.js
+++ b/index.js
@@ -284,3 +284,45 @@ export function escapePath(path) {
 
 	return path.replace(/[\\.[]/g, '\\$&');
 }
+
+// The keys returned by Object.entries() for arrays are strings
+function entries(value) {
+	if (Array.isArray(value)) {
+		return value.map((value, index) => [index, value]);
+	}
+
+	return Object.entries(value);
+}
+
+function stringifyPath(pathSegments) {
+	let result = '';
+
+	for (let [index, segment] of entries(pathSegments)) {
+		if (typeof segment === 'number') {
+			result += `[${segment}]`;
+		} else {
+			segment = escapePath(segment);
+			result += index === 0 ? segment : `.${segment}`;
+		}
+	}
+
+	return result;
+}
+
+function * deepKeysIterator(object, currentPath = []) {
+	if (!isObject(object)) {
+		if (currentPath.length > 0) {
+			yield stringifyPath(currentPath);
+		}
+
+		return;
+	}
+
+	for (const [key, value] of entries(object)) {
+		yield * deepKeysIterator(value, [...currentPath, key]);
+	}
+}
+
+export function deepKeys(object) {
+	return [...deepKeysIterator(object)];
+}
diff --git a/readme.md b/readme.md
index 828d09c..06d0f49 100644
--- a/readme.md
+++ b/readme.md
@@ -108,6 +108,29 @@ console.log(getProperty(object, escapedPath));
 //=> '🍄 The princess is in another castle!'
 ```
 
+### deepKeys(object)
+
+Returns an array of every path. Plain objects are deeply recursed and are not themselves included.
+
+This can be useful to help flatten an object for an API that only accepts key-value pairs or for a tagged template literal.
+
+```js
+import {getProperty, deepKeys} from 'dot-prop';
+
+const user = {
+	name: {
+		first: 'Richie',
+		last: 'Bendall',
+	},
+};
+
+for (const property of deepKeys(user)) {
+	console.log(`${property}: ${getProperty(user, property)}`);
+	//=> name.first: Richie
+	//=> name.last: Bendall
+}
+```
+
 #### object
 
 Type: `object | array`


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 53d859c..842883c 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -1,5 +1,5 @@
 import {expectTypeOf} from 'expect-type';
-import {getProperty, setProperty, hasProperty, deleteProperty} from './index.js';
+import {getProperty, setProperty, hasProperty, deleteProperty, deepKeys} from './index.js';
 
 expectTypeOf(getProperty({foo: {bar: 'unicorn'}}, 'foo.bar')).toBeString();
 expectTypeOf(getProperty({foo: {bar: 'a'}}, 'foo.notDefined.deep')).toBeUndefined();
@@ -17,3 +17,5 @@ expectTypeOf(setProperty(object, 'foo.bar', 'b')).toEqualTypeOf(object);
 expectTypeOf(hasProperty({foo: {bar: 'unicorn'}}, 'foo.bar')).toEqualTypeOf<boolean>();
 
 expectTypeOf(deleteProperty({foo: {bar: 'a'}}, 'foo.bar')).toEqualTypeOf<boolean>();
+
+expectTypeOf(deepKeys({foo: {bar: 'a'}})).toEqualTypeOf<string[]>();
diff --git a/test.js b/test.js
index 5db7cc7..bc981d3 100644
--- a/test.js
+++ b/test.js
@@ -1,5 +1,5 @@
 import test from 'ava';
-import {getProperty, setProperty, hasProperty, deleteProperty, escapePath} from './index.js';
+import {getProperty, setProperty, hasProperty, deleteProperty, escapePath, deepKeys} from './index.js';
 
 test('getProperty', t => {
 	const fixture1 = {foo: {bar: 1}};
@@ -407,6 +407,42 @@ test('escapePath', t => {
 	});
 });
 
+test('deepKeys', t => {
+	const object = {
+		'a.b': {
+			c: {
+				d: [1, 2, 3],
+				e: '🦄',
+				f: 0,
+			},
+			'': {
+				a: 0,
+			},
+		},
+		'': {
+			a: 0,
+		},
+	};
+	const keys = deepKeys(object);
+
+	t.deepEqual(keys, [
+		'a\\.b.c.d[0]',
+		'a\\.b.c.d[1]',
+		'a\\.b.c.d[2]',
+		'a\\.b.c.e',
+		'a\\.b.c.f',
+		'a\\.b..a',
+		'.a',
+	]);
+
+	for (const key of keys) {
+		t.true(hasProperty(object, key));
+	}
+
+	t.deepEqual(deepKeys([]), []);
+	t.deepEqual(deepKeys(0), []);
+});
+
 test('prevent setting/getting `__proto__`', t => {
 	setProperty({}, '__proto__.unicorn', '🦄');
 	t.not({}.unicorn, '🦄'); // eslint-disable-line no-use-extend-native/no-use-extend-native